2026年6月〜8月のツールチェーン更新:Gemini CLI から Antigravity CLI へ
このパイプラインを最初に組んだのは2026年5月でした。その後、足回りがひとつ大きく変わっています。Gemini CLI と個人向けの Gemini Code Assist IDE 拡張は6月18日で受付を終え、コマンドライン体験は Antigravity CLI へ統合されました。
本文中で gemini を呼び出している箇所は、現在は Antigravity CLI 側のエージェント実行に置き換えて運用しています。新しい CLI は Go で書き直されており、体感の応答が明確に速くなりました。夜間にまとめて回すローカライゼーションのように、待ち時間が積み上がる工程ほど恩恵が大きいです。
あわせて、I/O 2026 で発表された Antigravity 2.0 では、デスクトップアプリ・CLI・SDK・Managed Agents API・エンタープライズ向けデプロイの5つが同じエージェントハーネスを共有するようになりました。つまり、デスクトップで設計したエージェントの挙動が、そのまま CLI や API 側にも引き継がれます。本記事の AGENTS.md 設計はこの新しい構成でも有効です。むしろ、複数エージェントの並列実行とバックグラウンドのスケジュール実行がプラットフォーム標準の機能になったことで、.antigravity/tasks.json で書いていた夜間実行はより素直に組めるようになっています。
料金についても触れておきます。パブリックプレビュー中は無料で、AI Ultra プラン(月100ドル)では利用上限が AI Pro の5倍になります。夜間の連続実行を増やすほど、この上限が効いてきます。
移行で気をつけた点をひとつだけ挙げるなら、既存の自動化を一度に載せ替えないことです。私は CLI を呼ぶ箇所をひとつずつ Antigravity CLI に差し替え、出力が以前と同じ品質かを確認しながら進めました。大きな更新の初期は、いちばん壊れて困る工程を最後に回すのが安全だと感じています。
8月に入ってから足回りに効いた変更
その後の数週間でも、パイプラインの運転席まわりが動いています。ハブ 2.10.0(8月24日)で埋め込みターミナル(Ctrl/Cmd + バッククォート)と Review ペインでの Git 操作が入り、エージェントが書いた差分を「読んでからステージしてコミットする」という往復がアプリ内で閉じるようになりました。私の場合、この往復こそが一番外部ツールへ出ていた工程です。
CLI 側は 1.1.21(8月26日)が大きく、エージェントのコード検索用に ripgrep を同梱し、always-proceed モードで MCP のツール呼び出しとページ読み取りを自動承認するようになりました。非対話で回す夜間実行では、承認待ちが最大の停止要因です。ただし承認をどこまで外すかは、便利さに任せず自分で線を引く部分だと考えています。あわせて allow-always の提案が npm run や cargo run といったスクリプト名単位に絞られるようになったのは、一度通した広すぎる許可を後から狭めるのが難しいことを思えば、地味ですが効く変更です。
そして本記事の後半で使うのが、同じ 1.1.21 で入った丸めなしの cost フィールドです。
Phase 1: アイデア検証とUI設計の自動化
市場調査エージェントでアイデアを定量評価する
感覚ではなくデータでアイデアを評価するために、まず市場調査エージェントを定義します。
# AGENTS.md — Phase 1: Idea Validator
## market-researcher
あなたはアプリアイデアを定量評価するエージェントです。
App Store / Google Play のトレンドデータをWeb検索で収集し、以下の評価を行ってください。
評価項目:
1. 市場規模(カテゴリランキングの安定性・競合アプリの評価数推移)
2. 技術的実現可能性(ひとりの開発者が4週間以内に完成できるか)
3. 収益化の見通し(AdMob実績CPM・サブスクリプション相場・買い切り価格帯)
4. 差別化ポイント(既存上位アプリにない価値を提供できるか)
5. ASO難易度(主要キーワードの競争率)
出力形式: Markdownのスコアシート + 推奨カテゴリキーワード + 見送り推奨の場合の代替案
このエージェントに壁紙アプリのアイデアを渡すと、数分でレポートが返ってきます。
# Antigravityのターミナルから実行
antigravity agent run market-researcher \
--input "高解像度の自然写真を毎日1枚配信する壁紙アプリ。オフライン保存機能付き。日本の四季に特化。"
出力例(要約):
## アイデア評価レポート — スコア: 8.2/10
強み:
- 壁紙カテゴリはApp Storeでの安定した需要が確認できる(PhotoカテゴリTop 100常連)
- 「日本の四季」ニッチ化により競合差別化が明確
- AdMob壁紙切り替えタイミングとの相性が極めて良好(推定CPM: $3〜8)
- SwiftUI + Cloudflare R2で実装可能(推定工数: 3週間)
リスク:
- 写真素材の継続調達(Unsplash APIまたは自前撮影を検討)
- 季節性があるため、年間を通じたコンテンツ計画が必要
推奨キーワード(ASO): 壁紙, 高解像度, 日本, 四季, 風景, きれい
UIプロトタイプの自動生成(Stitch MCP連携)
アイデアが承認されたら、Antigravity のMCPを通じて Stitch に接続し、UIプロトタイプを生成します。
.antigravity/mcp-config.json は次のようになります。
{
"mcpServers" : {
"stitch" : {
"command" : "npx" ,
"args" : [ "@google/stitch-mcp" ],
"env" : {
"GOOGLE_AI_API_KEY" : "YOUR_GEMINI_API_KEY"
}
}
}
}
Antigravity のチャットで自然言語で指示するだけです。
User: @stitch 壁紙アプリのメイン画面と設定画面を作成してください。
デザイン要件:
- ダークモード/ライトモード両対応
- メイン画面: 全画面壁紙表示、下部に半透明のツールバー(お気に入り・保存・シェアアイコン)
- 設定画面: 通知設定、画質設定、ウィジェット設定
- フォント: SF Pro Display(iOS標準)
- アクセントカラー: #2C7BE5(青系)
生成されたUIデザインはAntigravity内で直接確認でき、修正も自然言語で行えます。
自然言語で直せるということは、裏を返せば指示の粒度がそのまま成果物の質になるということです。どこまで書けば意図どおりに返ってくるのかは、Antigravity のハーネスエンジニアリング — 指示設計の実践 に整理しました。
Phase 2: コア実装とテスト自動化
エージェントの分業設計:AGENTS.md の核心
実装フェーズでは、Antigravity のマルチエージェント機能を使って専門化されたエージェントが分業します。
# AGENTS.md — Phase 2: Implementation Team
## ui-builder
役割: Swift / Kotlin のUIコンポーネントを生成する
制約:
- SwiftUI / Jetpack Compose のみ使用(UIKit / XML禁止)
- すべての画像にaccessibilityLabelを設定すること
- Stitch の出力JSONを入力として受け取り、実際のコードに変換する
- Dynamic Typeに対応し、最大フォントサイズでもレイアウトが崩れないこと
出力: src/Views/ 以下に配置
## data-layer-engineer
役割: データ永続化とCloudKit同期を実装する
制約:
- SwiftData(Core Data不可)を使用する
- CloudKit同期は CKContainer で行い、プライベートデータベースのみ使用する
- オフライン時はローカルキャッシュから表示し、同期は復帰後にバックグラウンドで行う
- iCloud未設定ユーザーはローカル保存のみで動作すること
出力: src/Models/ および src/DataLayer/ 以下に配置
## test-writer
役割: 実装されたコードに対してXCTestを自動生成する
制約:
- 正常系・異常系・Edge caseを必ず含めること
- カバレッジ目標は80%以上
- Mock/Stubは DIコンテナを使い、実際のネットワーク接続は行わない
- async/awaitに対応したXCTestを書くこと
## localizer
役割: アプリ内テキストの多言語対応
対応言語: 日本語(基準)、英語、中国語(簡体字)、韓国語、スペイン語
制約:
- 機械翻訳ではなく自然な表現で書くこと
- App Storeの審査に引っかかる可能性のある表現を使わないこと
- Localizable.strings形式で出力する
Background Agent による夜間実装
昼間は別の作業をしながら、Antigravity の Background Agent にコア実装を任せます。
.antigravity/tasks.json に夜間タスクを並べます。
{
"tasks" : [
{
"id" : "overnight-data-layer" ,
"description" : "壁紙アプリのSwiftData + CloudKit 同期レイヤーを実装する。WallpaperItem モデルと WallpaperRepository クラスを作成し、ローカルキャッシュとCloudKit同期の両方に対応すること。" ,
"agent" : "data-layer-engineer" ,
"schedule" : "22:00 JST" ,
"outputDir" : "./WallpaperApp/Models/" ,
"validation" : "swift build && swift test WallpaperAppTests/DataLayerTests"
},
{
"id" : "overnight-ui" ,
"description" : "Stitch が生成した UIデザイン(./stitch-output/main-screen.json)をSwiftUIコードに変換する。" ,
"agent" : "ui-builder" ,
"schedule" : "22:30 JST" ,
"outputDir" : "./WallpaperApp/Views/" ,
"validation" : "swift build"
},
{
"id" : "overnight-l10n" ,
"description" : "新規追加された Localizable.strings のキーを英語・中国語(簡体字)・韓国語・スペイン語へ展開する。" ,
"agent" : "localizer" ,
"schedule" : "23:00 JST" ,
"outputDir" : "./WallpaperApp/Resources/" ,
"validation" : "python3 scripts/validate_strings.py WallpaperApp/Resources/Base.lproj/Localizable.strings WallpaperApp/Resources/*.lproj/Localizable.strings"
}
]
}
朝起きると、実装が完成してテストもパスしている状態が実現します。ただしすべてを自動化しようとするのは危険 です。以下の点は必ず人間が確認してください。
セキュリティに関わる実装(認証・課金処理・APIキーの扱い)
ユーザーのプライバシーに関わるデータの扱い
パフォーマンスのボトルネック(Instrumentsでのプロファイリング)
テスト自動化の実際
test-writer エージェントが生成するXCTestの例です。
// WallpaperFetcherTests.swift(Antigravityが自動生成)
import XCTest
@testable import WallpaperApp
@MainActor
final class WallpaperFetcherTests : XCTestCase {
// 正常系: 画像URLが正しく生成される
func testFetchWallpaperURL_WithValidDate_ReturnsCorrectURL () async throws {
let fetcher = WallpaperFetcher (
baseURL : URL ( string : "https://cdn.example.com" ) !
)
let date = Calendar.current. date (
from : DateComponents ( year : 2026 , month : 5 , day : 6 )
) !
let url = try await fetcher. fetchURL ( for : date)
XCTAssertEqual (url.host, "cdn.example.com" )
XCTAssertTrue (url.path. hasSuffix ( "2026-05-06.jpg" ))
}
// Edge case: オフライン時にキャッシュからフォールバック
func testFetch_WhenOffline_ReturnsCachedURL () async throws {
let mockMonitor = MockNetworkMonitor ( isConnected : false )
let cache = WallpaperCache ()
let cachedURL = URL ( string : "https://cdn.example.com/cached.jpg" ) !
await cache. set ( key : "latest" , value : cachedURL)
let fetcher = WallpaperFetcher (
networkMonitor : mockMonitor,
cache : cache
)
let result = try await fetcher. fetchURL ( for : Date ())
XCTAssertEqual (result, cachedURL)
}
// 異常系: 未来日付でWallpaperFetchErrorが返る
func testFetch_WithFutureDate_ThrowsError () async {
let fetcher = WallpaperFetcher (
baseURL : URL ( string : "https://cdn.example.com" ) !
)
let futureDate = Date (). addingTimeInterval ( 86400 * 30 )
do {
_ = try await fetcher. fetchURL ( for : futureDate)
XCTFail ( "エラーが発生するはずです" )
} catch let error as WallpaperFetchError {
XCTAssertEqual (error, .futureDateNotAllowed)
} catch {
XCTFail ( "予期しないエラー: \( error ) " )
}
}
}
Phase 3: マーケティング素材の自動生成
ここが、このパイプラインで最も時間削減効果が大きいフェーズです。従来、App Storeのスクリーンショットとプロモーション動画の制作には、デザイナーとの協業で2〜3日かかっていました。
スクリーンショットの自動生成(Stitch + Fastlane)
Stitch で作成したUIプロトタイプをベースに、各デバイスサイズのスクリーンショットを自動生成します。
# fastlane/Fastfile(Antigravityが生成・管理)
lane :generate_screenshots do
# Stitch MCPからスクリーンショットテンプレートを取得
sh ( "python3 scripts/fetch_stitch_frames.py" )
# Simulatorでスクリーンショットをキャプチャ
capture_screenshots (
workspace: "WallpaperApp.xcworkspace" ,
scheme: "WallpaperAppUITests" ,
output_directory: "fastlane/screenshots" ,
devices: [
"iPhone 16 Pro Max" ,
"iPhone SE (3rd generation)" ,
"iPad Pro (12.9-inch) (6th generation)"
],
languages: [ "ja-JP" , "en-US" , "zh-Hans" , "ko-KR" ],
override_status_bar: true ,
concurrent_simulators: true
)
# Stitchでブランドフレームをオーバーレイ
sh ( "python3 scripts/apply_brand_overlay.py \
--input fastlane/screenshots \
--template stitch-output/screenshot-frame.json \
--output fastlane/framed_screenshots" )
frame_screenshots ( path: "fastlane/framed_screenshots" )
end
プロモーション動画の生成(Veo 3 API)
Veo 3 API を使ったプロモーション動画の自動生成は、Antigravity エージェントが Python スクリプトを介して実行します。
# scripts/generate_promo_video.py
"""
Antigravityエージェントが呼び出すVeo 3動画生成スクリプト
App Store Preview動画(最大30秒)を3バリエーション生成してA/Bテスト用に使用する
"""
import time
from google import genai
from pathlib import Path
client = genai.Client( api_key = "YOUR_GEMINI_API_KEY" )
PROMO_PROMPTS = {
"morning" : """
30秒のアプリプロモーション動画。朝の柔らかい光の中で:
1. スマートフォンのロック画面が映り、美しい山の壁紙が表示される(5秒)
2. ゆっくりとスワイプして桜の壁紙に切り替わる(8秒)
3. 「お気に入り」ボタンをタップして保存する操作(5秒)
4. ホーム画面に設定された壁紙がリビングの雰囲気と溶け込む(8秒)
5. アプリアイコンと「毎日新しい日本の風景」というキャッチコピー(4秒)
スタイル: シネマティック、暖かみのある色調、BGMなし(無音)
""" ,
"zen" : """
30秒のアプリプロモーション動画。禅・ミニマリストテーマ:
画面中央に壁紙が静かに現れ、日本の庭・寺・雪景色が静かに切り替わる。
シンプルで落ち着いた演出。字幕: 「静かな日本を、毎日手のひらに」
スタイル: 極めてシンプル、モノクロームに近い色調
""" ,
"energy" : """
30秒のアプリプロモーション動画。活力・元気テーマ:
様々な季節の日本の風景が素早く切り替わる(各2秒)。
桜→新緑→花火→紅葉→雪の順番。最後にアプリUIが表示され
「毎日が新しい。毎日が日本。」のキャッチコピーで締める。
スタイル: 明るく鮮やか、テンポ感のある演出
"""
}
def generate_promo_videos ():
operations = []
for variant, prompt in PROMO_PROMPTS .items():
print ( f "動画生成開始: { variant } " )
operation = client.models.generate_videos(
model = "veo-3-0-generate-preview" ,
prompt = prompt,
config = {
"numberOfVideos" : 1 ,
"durationSeconds" : 30 ,
"aspectRatio" : "9:16" , # iPhoneのApp Previewは縦向き
"resolution" : "1080p" ,
}
)
operations.append((variant, operation))
time.sleep( 2 ) # API rate limit対策
# 全動画の生成完了を待機
results = {}
for variant, op in operations:
while not op.done:
time.sleep( 15 )
op = client.operations.get(op)
video_uri = op.response.generated_videos[ 0 ].video.uri
output_path = Path( f "fastlane/promo_videos/ { variant } .mp4" )
output_path.parent.mkdir( exist_ok = True )
# ダウンロードして保存
import urllib.request
urllib.request.urlretrieve(video_uri, output_path)
print ( f "✅ { variant } : { output_path } " )
results[variant] = str (output_path)
return results
if __name__ == "__main__" :
generate_promo_videos()
素材づくりそのものをエージェントに預けると何が起きるかは、App Store スクリーンショットの多言語更新を4本のアプリで5週間任せた所感 に書きました。うまく回った部分と、結局手で直した部分の両方を残しています。
Phase 4: ストア申請とASO最適化
ストア説明文のローカライゼーション(Gemini CLI)
#!/bin/bash
# scripts/generate_metadata.sh — Antigravity が管理
APP_NAME = "きせかえ壁紙 — 日本の四季"
APP_DESCRIPTION = $( cat docs/app-description-ja.md )
for lang in "en-US" "zh-Hans" "ko-KR" "es-ES" ; do
echo "Generating metadata for: $lang "
antigravity agent run localizer --input "
以下の日本語アプリ説明文を${ lang }向けに最適化して翻訳してください。
制約:
- App Storeの審査ガイドライン準拠
- 各言語のユーザーに自然な表現を使う(直訳不可)
- 主要キーワードを最初の段落に含める
- 文字数: 4,000文字以内
アプリ名: ${ APP_NAME }
説明文(日本語原文):
${ APP_DESCRIPTION }
" > "fastlane/metadata/${ lang }/description.txt"
sleep 2 # API rate limit対策
done
App Store Connect API による申請の完全自動化
# fastlane/Fastfile(申請レーン)
lane :submit_for_review do
# バージョン番号を自動インクリメント
increment_build_number (
build_number: latest_testflight_build_number + 1
)
# TestFlightで社内テスト
build_app ( scheme: "WallpaperApp" , export_method: "app-store" )
upload_to_testflight (
skip_waiting_for_build_processing: false ,
changelog: sh ( "git log --oneline -10" ). strip
)
# 48時間の社内テスト後、本番申請
deliver (
submit_for_review: true ,
automatic_release: false , # 手動承認で慎重に
metadata_path: "./fastlane/metadata" ,
screenshots_path: "./fastlane/framed_screenshots" ,
app_review_information: {
demo_user: "demo@example.com" ,
demo_password: "Demo1234!" ,
notes: "デモアカウントでログインするとすべての機能をテストできます"
},
submission_information: {
add_id_info_uses_idfa: false ,
export_compliance_uses_encryption: false ,
content_rights_has_rights: true ,
content_rights_contains_third_party_content: false
}
)
end
申請フローをもう一段細かく組み込みたい場合は、Antigravity で構築する iOS アプリ全自動リリースパイプライン がこの節の続きになります。
パイプラインを支えるCI/CDの設計
GitHub Actions との連携
コードのプッシュをトリガーに、テスト・ビルド・配信まで自動で流れる設定です。
# .github/workflows/ios-pipeline.yml
name : iOS CI/CD Pipeline
on :
push :
branches : [ main , 'release/*' ]
pull_request :
branches : [ main ]
jobs :
test :
runs-on : macos-15
steps :
- uses : actions/checkout@v4
- name : Set up Xcode 26
run : sudo xcode-select -s /Applications/Xcode_26.app
- name : Run tests
run : |
xcodebuild test \
-workspace WallpaperApp.xcworkspace \
-scheme WallpaperApp \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-resultBundlePath TestResults.xcresult
- name : Upload test results
uses : actions/upload-artifact@v4
with :
name : test-results
path : TestResults.xcresult
deploy-testflight :
needs : test
if : github.ref == 'refs/heads/main'
runs-on : macos-15
steps :
- uses : actions/checkout@v4
- name : Install Fastlane
run : gem install fastlane
- name : Deploy to TestFlight
env :
APP_STORE_CONNECT_API_KEY_ID : ${{ secrets.API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID : ${{ secrets.ISSUER_ID }}
APP_STORE_CONNECT_KEY : ${{ secrets.API_KEY }}
run : fastlane deploy_testflight
実際の落とし穴と対策
このパイプラインを実際に運用していて遭遇した落とし穴を共有します。
落とし穴1: AIが生成したコードのアクセシビリティ漏れ
Antigravityが生成するSwiftUIコードは機能的には正しいことが多いですが、VoiceOverのラベルが空になることがあります。
// ❌ よくあるAI生成コードの問題
Image (wallpaper.imageName)
. resizable ()
. aspectRatio ( contentMode : .fill)
. ignoresSafeArea ()
// ✅ アクセシビリティを考慮した正しい実装
Image (wallpaper.imageName)
. resizable ()
. aspectRatio ( contentMode : .fill)
. ignoresSafeArea ()
. accessibilityLabel ( " \( wallpaper. title ) — \( wallpaper. location ) " )
. accessibilityAddTraits (.isImage)
AGENTS.md に「すべての対話可能な要素とセマンティックに重要な画像にaccessibilityLabelを必ず設定すること」と明示することで防げます。
落とし穴2: Gemini CLIの出力をそのままストアに使わない
Gemini CLI が生成するストア説明文は品質が高いですが、必ず人間によるレビューを経てから申請してください。特に以下の点を確認します。
「世界No.1」「最高の品質」などの誇大表現がないか
競合他社を名指しで比較していないか(Apple の審査でリジェクトされる)
プライバシーポリシーと矛盾する記述がないか
落とし穴3: Veo 3で生成した動画の著作権確認
Veo 3 が生成した動画のライセンスはGoogleの利用規約に準拠します。App Storeに申請する前に、商用利用が許可されているか最新の規約を確認してください。また、App Preview動画には音声が含まれていると審査で問題になる場合があるため、BGMなし(無音)での生成をお勧めします。
落とし穴4: Background Agentが途中で停止する長時間タスク
Background Agentは長時間のタスクで途中停止することがあります。タスクをチェックポイント付きで設計する点が肝心です。
# AGENTS.md — タスク分割の原則
長時間タスクは必ず30分以内に完了するサブタスクに分割すること。
例:
❌ 「アプリ全体の実装」(数時間かかる可能性)
✅ 「WallpaperFetcherクラスのみを実装する」(20〜30分)
✅ 「WallpaperListViewのUIのみを実装する」(20〜30分)
各サブタスクの完了後にコミットし、次のサブタスクで前のコミットを参照すること。
夜間の localizer 出力を、朝そのまま信じてよいか — 書式指定子のずれを測ってみました
先ほどの .antigravity/tasks.json を読み返すと、実装系のタスクには swift build や swift test という validation が付いています。ところが localizer には、長いあいだ何も付けていませんでした。
理由は単純で、翻訳の良し悪しは機械で測れないと思い込んでいたからです。けれど翻訳の「質」と、文字列リソースの「構造」はまったく別物でした。Localizable.strings は Xcode のビルドを通っても中身を検証されません。String(format:) が引数を解決するのは実行時です。つまり %1$@ と %2$@ の順序がひとつ入れ替わっただけの訳文は、ビルドも通り、テストも通り、自分が読める言語の画面では何も起きないまま出荷されます。
私がこれに気づいたのは、スペイン語のスクリーンショットで数字の位置が妙だと感じたときでした。夜間に4言語ぶん生成されたファイルを、朝に目視で全部追うのは現実的ではありません。そこで、翻訳の質ではなく構造だけを見るゲートを書きました。
構造だけを見る検証スクリプト
#!/usr/bin/env python3
"""Localizable.strings の書式指定子ドリフトを検出するゲート。
base ロケール(通常 en か ja)を正とし、各訳文の指定子集合と位置引数の整合を検査する。
標準ライブラリのみ。scripts/validate_strings.py として置いて CI から呼ぶ。
"""
from __future__ import annotations
import re, sys, os
from collections import Counter
# "key" = "value"; (エスケープされた \" を許容)
ENTRY = re.compile( r ' ^\s * " ((?:[ ^ " \\ ] | \\ .) * ) " \s * = \s * " ((?:[ ^ " \\ ] | \\ .) * ) " \s * ;' , re.M)
# %@ %d %1$@ %.2f %% などを拾う
SPEC = re.compile(
r '% (?:(\d + ) \$ ) ? ([ -+ #0 ] * )(\d +| \* ) ? (?: \. (\d +| \* )) ? '
r ' ( hh | h | ll | l | q | L | z | j | t ) ? ([ @dDiuUxXoOfeEgGcCsSpaAF% ]) '
)
def parse (path: str ) -> dict[ str , str ]:
with open (path, encoding = 'utf-8-sig' ) as f:
src = f.read()
src = re.sub( r '/ \* . *? \* /' , '' , src, flags = re.S)
return {m.group( 1 ): m.group( 2 ) for m in ENTRY .finditer(src)}
def specs (value: str ) -> list[ tuple ]:
out = []
for m in SPEC .finditer(value):
if m.group( 6 ) == '%' : # %% はリテラルなので対象外
continue
out.append((m.group( 1 ), m.group( 6 ))) # (位置番号 or None, 変換子)
return out
def check (base: str , tr: str ) -> list[ str ]:
b, t = specs(base), specs(tr)
errs = []
bpos = any (p for p, _ in b)
tpos = any (p for p, _ in t)
if len (b) != len (t):
errs.append( f 'count { len (b) } -> { len (t) } ' )
if bpos != tpos and (b or t):
errs.append( 'positional-mix' ) # 片側だけ %1$@ 形式になっている
if bpos and tpos:
bm = { int (p): c for p, c in b if p}
tm = { int (p): c for p, c in t if p}
if set (bm) != set (tm):
errs.append( f 'arg-index { sorted (bm) } -> { sorted (tm) } ' )
else :
for i in sorted (bm):
if bm[i] != tm[i]:
errs.append( f 'arg { i } type % { bm[i] } ->% { tm[i] } ' )
else :
# 非位置指定は並び順そのものが引数の対応になる
bt = [c for _, c in b]
tt = [c for _, c in t]
if bt != tt:
if Counter(bt) == Counter(tt):
errs.append( f 'order { "" .join(bt) } -> { "" .join(tt) } ' )
else :
errs.append( f 'type { "" .join(bt) or "-" } -> { "" .join(tt) or "-" } ' )
return errs
def run (base_path: str , targets: list[ str ]) -> int :
base = parse(base_path)
bad = 0
for tp in targets:
tr = parse(tp)
loc = os.path.basename(os.path.dirname(tp)) or tp
for key, bval in base.items():
if key not in tr:
print ( f ' { loc } : [ { key } ] MISSING' ); bad += 1 ; continue
for e in check(bval, tr[key]):
print ( f ' { loc } : [ { key } ] { e } ' ); bad += 1
for key in tr.keys() - base.keys():
print ( f ' { loc } : [ { key } ] ORPHAN' ); bad += 1
return bad
if __name__ == '__main__' :
if len (sys.argv) < 3 :
sys.exit( 'usage: validate_strings.py <base.strings> <target.strings>...' )
n = run(sys.argv[ 1 ], sys.argv[ 2 :])
print ( f '--- { n } issue(s)' )
sys.exit( 1 if n else 0 )
書式指定子だけを比べ、訳文そのものには一切触れていません。翻訳を評価するのではなく、翻訳が壊してはいけない骨格だけを守る、という割り切りです。
どのずれを捕まえられて、どれを捕まえられないのか
書いたゲートを信用してよいかは、実際に壊してみないと分かりません。壁紙アプリで実際に使っている形に近い10キーの base ファイルを用意し、夜間出力で起こりうるずれを分類ごとに1件ずつ注入して、検出できるかを機械的に数えました。以下は手元の VM で回した実測値です(本番のテレメトリではなく、注入した変異に対する検出率です)。
ずれの分類 注入 検出 検出率
引数の取りこぼし 9 9 100%
引数の重複 9 9 100%
位置指定の剥落(%1$@→%@) 3 3 100%
位置指定の後付け(%@→%1$@) 5 5 100%
変換子の型違い(%d→%@) 4 4 100%
%% を % に潰す1 1 100%
全角化(%@ / %d) 5 5 100%
位置引数の入替(型が異なる) 1 1 100%
位置引数の入替(型が同じ) 2 0 0%
精度の欠落(%.1f→%f) 1 0 0%
キー欠落 10 10 100%
余剰キー 10 10 100%
合計 60 57 95.0%
訳文だけを変えた健全なケース(誤検出の確認) 10 0 0%
健全な訳文変更で1件も鳴らなかったのは重要です。誤検出が出るゲートは、数日で --skip を付けられて死にます。
意味があったのは、むしろ捕まえられなかった側でした。%1$@ と %2$@ のように型が同じ位置引数 が入れ替わったケースは、静的には原理的に検出できません。指定子の集合も、型も、個数も、すべて一致してしまうからです。この10キーのうち2キーが、その条件に当てはまっていました。
同型の位置引数を2つ以上持つキー: download_progress, quota_left (2/10)
"Downloading %1$@ of %2$@" が "%2$@ の %1$@ をダウンロード中" になっても、ゲートは無言で通します。表示は「12 の 3 をダウンロード中」になり、クラッシュもしません。落ちてくれないぶん、こちらのほうが厄介です。
測ったあとに変えた3つのこと
数字を見て、運用のほうを変えました。
ひとつめは、同型の位置引数を2つ並べる文言を base 側で作らないことです。"%1$@ / %2$@" のような文字列は、単位や語を挟んで型が違う形(片方を %1$d にする)に書き換えるか、2つのキーに分けます。静的に検出できない形を、そもそも入力しない。これがいちばん確実でした。
ふたつめは、その条件に当てはまるキーを一覧で出しておき、スクリーンショット生成の対象に必ず含めることです。Phase 3 で 5言語ぶんのスクリーンショットを自動生成しているので、そこに該当画面を混ぜておけば、朝に画像を並べて眺めるだけで数字の位置の異常に気づけます。機械で無理な部分だけを目視に回す、という配分です。
みっつめは、tasks.json の localizer に validation を付けたことです。先ほどの overnight-l10n がそれで、検証に落ちれば夜間タスク自体が失敗として残ります。朝いちばんに「成功したが中身が壊れている」より、「失敗している」と分かるほうがずっと扱いやすいです。
CI 側にも同じ一行を置いています。
# .github/workflows/ci.yml(該当ステップのみ)
- name : Validate localization structure
run : |
python3 scripts/validate_strings.py \
WallpaperApp/Resources/Base.lproj/Localizable.strings \
WallpaperApp/Resources/*.lproj/Localizable.strings
こうしたずれを毎朝目視で拾い続けるのは現実的ではありません。受け入れ条件をプロンプト側へ先に書き込んでおくほうが確実で、その書き方はAntigravity でエージェントと「完了」を合意する にまとめてあります。
このパイプラインで一番学んだのは、自動化した工程には自動化した検査を必ず一緒に置く、という当たり前のことでした。生成の速度を上げるほど、目視で追える量との差は開いていきます。95% は満点ではありませんが、残りの5%が何なのかを言葉にできたことのほうが、私にとっては価値がありました。
エージェントに任せる範囲を、私はこう線引きしています
このパイプラインで一番よく聞かれるのは、ツールの設定方法ではなく「どこまでをエージェントに任せているのか」という点です。私自身、個人開発を続けてきたなかで、ここの線引きを何度も引き直してきました。
判断の軸はシンプルです。規約・仕様・過去の決定に照らせば正解が一意に決まる作業は、迷わずエージェントに渡します。テストの雛形生成、Localizable.strings の整形、スクリーンショットの量産、リリースノートの下書き。これらは「正しさ」が外側の基準で測れるので、私が確認するのは抜けがないかだけで済みます。
逆に、正解が私の中にしかない作業は手元に残します。どのアイデアを次に出すか、価格をいくらにするか、説明文のどの一文を主役にするか。ここはユーザーへの価値判断そのものなので、エージェントの提案は素材として受け取り、最終的な言葉は自分で選びます。
もうひとつ、必ず人間側に置いているのがセキュリティと課金の境界です。認証フロー、APIキーの取り回し、課金処理の検証は、動くコードが出てきても一行ずつ自分の目で追います。ここでの一度の見落としは、後から自動化で取り返せる種類のものではないからです。
複数のプロジェクトをバックグラウンドのスケジュールで並行して回していると、エージェントは静かに大量のアウトプットを積み上げてくれます。だからこそ、積み上がったものを採用するかどうかを決める人間の判断が、以前よりむしろ重くなったと感じています。自動化が進むほど、最後に残るのは判断の質なのだと思います。
継続的改善:データドリブンな運用
リリース後の分析自動化
# scripts/weekly_report.py
"""毎週月曜にスケジュール実行するレポート生成スクリプト。"""
import json
import subprocess
import sys
def fetch_metrics (bundle_id: str ) -> dict :
"""fastlane 経由で App Store Connect から週次の実績を取得する。"""
result = subprocess.run(
[ "fastlane" , "run" , "app_store_connect" ,
"action:get_app_info" ,
f "app_identifier: { bundle_id } " ],
capture_output = True , text = True ,
)
if result.returncode != 0 :
raise RuntimeError ( f "fastlane failed: { result.stderr.strip()[: 200 ] } " )
return json.loads(result.stdout)
def generate_weekly_report (m: dict ) -> str :
prompt = f """
以下のアプリパフォーマンスデータを分析し、
次の2週間で優先すべき改善点を3つ挙げてください。
各改善点には具体的な実装手順も含めてください。
週次データ:
- ダウンロード数: { m[ 'downloads' ] }
- DAU: { m[ 'dau' ] }
- クラッシュ率: { m[ 'crash_rate' ] } %
- レビュー評価: { m[ 'avg_rating' ] } ( { m[ 'review_count' ] } 件)
- 主な不満点(ユーザーレビューより): { m[ 'top_complaints' ] }
"""
result = subprocess.run(
[ "antigravity" , "agent" , "run" , "release-analyst" , "--input" , prompt],
capture_output = True , text = True ,
)
if result.returncode != 0 :
raise RuntimeError ( f "agent failed: { result.stderr.strip()[: 200 ] } " )
return result.stdout
if __name__ == "__main__" :
try :
metrics = fetch_metrics( "com.example.wallpaper" )
print (generate_weekly_report(metrics))
except ( RuntimeError , KeyError , json.JSONDecodeError) as e:
# 失敗を空レポートで握り潰さない。月曜の朝に気づける形で落とす
print ( f "weekly_report failed: { e } " , file = sys.stderr)
sys.exit( 1 )
最初に書いたときは、この関数がプロンプトの f-string の中でだけ downloads や dau を参照していて、どこでも定義していませんでした。手元で一度も実行せずスケジュールに載せたので、毎週月曜の朝に静かに NameError で終わっていたのです。気づいたのは数週間後、レポートが一度も届いていないと分かったときでした。生成したコードを「読んで納得した」だけで通してしまうと、こういう抜けが残ります。
この週次レポートを手元のスケジュールからサーバー側へ移すなら、Cloud Run × Pub/Sub × BigQuery で組むサーバーレスAIパイプライン がそのまま受け皿になります。
パイプラインの実費を、フェーズ別に割ってみました
夜間実行を増やすほど、月末に手元へ残るのは合計金額だけになります。そこからは、どのフェーズが高くついているのかが読めません。工数の話は前半で散々してきたのに、費用の内訳は長いあいだ放置していました。
CLI 1.1.21 で入った丸めなしの cost フィールドは、この穴をふさぐために使えます。表示上は丸められていた値を、そのまま自前の集計へ流せるからです。
やることは2つだけです。ステータスラインのスクリプトから1ターンぶんの記録を JSONL へ追記し、あとでまとめて読む。フェーズの札は、ブランチ名か環境変数から付けています。
{"phase":"implement","turn":0,"cost":0.0464,"model":"agent"}
集計側に依存を足したくなかったので、Node の標準モジュールだけで書きました。
#!/usr/bin/env node
// phase-cost.mjs — 依存ゼロ。フェーズ別のセッションコストを集計する。
// 使い方: node phase-cost.mjs ~/.antigravity/cost-ledger.jsonl
import { readFileSync } from "node:fs" ;
const path = process.argv[ 2 ];
if ( ! path) {
console. error ( "usage: node phase-cost.mjs <ledger.jsonl>" );
process. exit ( 2 );
}
const rows = readFileSync (path, "utf8" )
. split ( " \n " )
. filter (( l ) => l. trim ())
. map (( l , i ) => {
try {
return JSON . parse (l);
} catch {
console. error ( `skip: 行 ${ i + 1 } が JSON として読めません` );
return null ;
}
})
. filter (( r ) => r && typeof r.cost === "number" && Number. isFinite (r.cost));
const agg = new Map ();
let exact = 0 ;
let rounded = 0 ;
for ( const r of rows) {
const phase = r.phase ?? "unlabeled" ;
const a = agg. get (phase) ?? { n: 0 , sum: 0 , max: 0 };
a.n += 1 ;
a.sum += r.cost;
a.max = Math. max (a.max, r.cost);
agg. set (phase, a);
exact += r.cost;
rounded += Math. round (r.cost * 100 ) / 100 ; // 丸めた表示だけを足し込んだ場合
}
const usd = ( v ) => `$${ v . toFixed ( 4 ) }` ;
const entries = [ ... agg. entries ()]. sort (( x , y ) => y[ 1 ].sum - x[ 1 ].sum);
console. log ( "phase runs total share avg/run max/run" );
for ( const [ phase , a ] of entries) {
console. log (
phase. padEnd ( 10 ) +
String (a.n). padStart ( 5 ) +
usd (a.sum). padStart ( 11 ) +
`${ (( a . sum / exact ) * 100 ). toFixed ( 1 ) }%` . padStart ( 9 ) +
usd (a.sum / a.n). padStart ( 10 ) +
usd (a.max). padStart ( 11 )
);
}
console. log ( "-" . repeat ( 56 ));
console. log ( `合計(丸めなし) ${ usd ( exact ) } ${ rows . length } runs` );
console. log ( `合計(各行を2桁に丸めて加算) ${ usd ( rounded ) }` );
const drift = rounded - exact;
console. log (
`丸め誤差 ${ usd ( drift ) }(${ (( drift / exact ) * 100 ). toFixed ( 2 ) }%・${ rows . length } 行の積み上げ)`
);
丸めの影響を測ったら、想像とは違う結果が出ました
丸めなしの値がありがたいのは、合計が正確になるからだ——と最初は思っていました。そこで、1,160 ターンぶんの台帳を2通り用意して実際に比べています。フェーズごとの本数は同じで、違うのは1ターンあたりの単価だけです。
台帳 1ターンの単価 丸めなし合計 各行を丸めて加算 ずれ
高めのターン中心 $0.0002〜$0.085 $35.5019 $35.4800 -0.06%
安いターン中心 $0.0002〜$0.0085 $3.5502 $2.8600 -19.44%
単価が表示桁より十分に大きいときは、丸めの上下がほぼ打ち消し合って 0.06% しか動きません。ところが単価が2桁の表示幅を下回りはじめると、切り捨てられた側が戻ってこず、同じ 1,160 行で約2割が消えました。
つまり丸めなしの cost が効くのは「合計の精度」ではありません。短い確認ターンが大量に積み上がる夜間実行ほど、丸めた表示では原価が実態より安く見える 、という一点です。このパイプラインはまさにその形をしています。
集計を回して最初に見えたこと
フェーズ別の内訳は次のようになりました。
phase runs total share avg/run max/run
implement 610 $28.2948 79.7% $0.0464 $0.0849
design 240 $4.0487 11.4% $0.0169 $0.0300
assets 180 $2.1159 6.0% $0.0118 $0.0209
release 130 $1.0424 2.9% $0.0080 $0.0140
implement が回数でも金額でも突出しています。ここを見る前の私は、素材生成のほうが高いはずだと思い込んでいました。Veo 3 の動画やスクリーンショットは「重い処理」という印象があるからです。実際には、重いのは1回あたりの単価ではなく、実装フェーズで細かく往復する回数のほうでした。
削るとしたら assets の解像度ではなく、implement の往復回数です。受け入れ条件を先に書いておく、差し戻しの単位を大きくする——どちらも回数に効きます。原価を測る前と後で、手を入れる場所が完全に入れ替わりました。
数字の出どころについて
正直に書いておくと、上の台帳は検証用に手元で生成したサンプルで、金額そのものは私の実際の請求額ではありません。確かめたのは集計ロジックと丸めの挙動です。生成側は seed を固定してあるので、同じスクリプトを回せば同じ数字が出ます。実行時間は 1,160 行で 0.04 秒(Node v22.23.2・3回とも同値)でした。
自分の請求額を知りたい場合は、phase の札の付け方だけ自分の運用に合わせて、あとは実データの JSONL を食わせてください。集計側は変えなくて構いません。
実際の成果と時間削減効果
このパイプラインを壁紙アプリに適用した実際のデータです。
Before(パイプライン導入前):
企画からリリースまで: 約8週間
スクリーンショット・動画制作: 3〜4日
ローカライゼーション(4言語): 5日〜1週間
アップデートサイクル: 3〜4週間
総工数: 250〜300時間
After(パイプライン導入後):
企画からリリースまで: 約2〜3週間
スクリーンショット・動画制作: 半日以下
ローカライゼーション(4言語): 1〜2時間
アップデートサイクル: 1週間以下
総工数: 60〜80時間
工数が1/4になった分、その時間を「次のアプリの企画」と「ユーザーフィードバックへの対応」に使えるようになりました。
空いた時間の一部は、リリース後のユーザー対応にも回しています。任せてよかった作業と、任せて後悔した作業の線引きはAIに任せた、そして後悔した — 個人開発者が気づいたエージェント委任の境界線 に書きました。
あなたの開発スタイルに合わせた導入ステップ
このパイプラインをすべて一度に導入しようとする必要はありません。まずは「一番時間を取られている作業」から始めることをお勧めします。
Week 1〜2: 環境構築
Antigravity + Stitch MCP の連携を設定し、UIプロトタイプ生成だけを自動化する
Week 3〜4: 実装自動化
AGENTS.md でエージェントを定義し、Background Agentを使った夜間実装を試す
Week 5〜6: テスト自動化
test-writer エージェントでXCTestを自動生成し、GitHub Actionsと連携させる
Week 7〜8: マーケティング自動化
Fastlane + Stitch でスクリーンショット生成を自動化し、Gemini CLIでストア文書を生成する
Week 9以降: Veo 3統合
プロモーション動画の自動生成を導入し、パイプラインを完成させる
AI技術が急速に進化する今、ひとりの開発者が実現できることの幅は毎月広がっています。このパイプラインも、1年前には存在しなかった技術の組み合わせです。
ひとりでも「スタジオ」を持てる時代に、あなたのアプリがより多くのユーザーに届けられることを願っています。このガイドが、その第一歩の参考になれば幸いです。