Büyük bir iOS/Android kod tabanında AI ajanlarını çalıştırmak: proje kuralları, bağlam ve sınırlar

Kodlama ajanları küçük repoda parlıyor, 100 modüllü mobil uygulamada tökezliyor. Fark çoğu zaman repo'nun hazırlığından geliyor. Tek modüllük build ve test komutları, modül başına kural dosyaları, bağlam sınırı ve ajana verilmeyecek yetkiler üzerinden uçtan uca anlatıyoruz.

Bir kodlama ajanını üç ekranlı bir demo projede denediğinizde sonuç etkileyicidir: görevi verirsiniz, build alır, testi yazar, PR açar. Aynı ajanı 110 modüllü bir seyahat rezervasyon uygulamasına soktuğunuzda ilk yarım saatini repo'yu baştan sona grep'lemekle, ikinci yarım saatini tüm uygulamayı build etmekle geçirir, sonunda da project.pbxproj dosyasını elle düzenleyip projeyi açılmaz hâle getirir. Model aynı model. Değişen şey, ajanın içinde çalıştığı ortam.

Bizim pozisyonumuz şu: büyük bir mobil kod tabanında ajan verimi, model seçiminden çok repo'nun ajana ne kadar hazır olduğuna bağlıdır. Yeni başlayan bir geliştiricinin ilk haftasında takıldığı her şey (hangi komutla build alınır, bu modül neye bağlanabilir, testler nerede) ajanı her oturumda yeniden durdurur, çünkü ajan her oturuma hafızası sıfırlanmış olarak başlar. Bu yazıda repo'yu nasıl hazırladığımızı, ajana neyi gösterip neyi vermediğimizi ve hangi işlerin henüz ajana verilmemesi gerektiğini anlatıyoruz. Örnek olarak hayali bir seyahat uygulaması olan Rota'yı ve onun fiyat alarmı (FareAlerts) özelliğini kullanacağız.

Büyük mobil repo ajanı neden yorar

Gerçekçi bir "önce" hâliyle başlayalım. Görev tek cümle, repo'da ajana özel hiçbir hazırlık yok:

Görev: "Fiyat alarmı ekranına 'alarmı duraklat' aksiyonu ekle."

Ajanın oturumu:
1. Kökten "FareAlert" araması: yüzlerce eşleşme, onlarca dosya okundu
   (yarısı eski Objective-C ekranı ve üretilmiş API istemcisi)
2. xcodebuild -workspace Rota.xcworkspace -scheme Rota build
   → bütün uygulama derlendi, on binlerce satır log bağlama aktı
3. Yeni Swift dosyasını hedefe eklemek için project.pbxproj elle düzenlendi
4. Build kırıldı, ajan signing ayarlarını "düzeltmeye" girişti
5. Testler UI test hedefiyle birlikte çalıştırıldı, simülatör açılmadı
6. Sonuç: 14 dosyada değişiklik, ikisi başka feature modülünde,
   biri üretilmiş kodda; PR açıklaması "tüm testler geçti" diyor

Bu tablodaki her adımın mobil dünyaya özgü bir sebebi var.

Geri bildirim döngüsü yavaştır. Bir backend servisinde test komutu saniyeler içinde döner; büyük bir iOS uygulamasında temiz build dakikalar alır, UI testleri simülatör ister, Android'de instrumented testler emülatör ister. Ajan deneme yanılmayla çalışan bir araçtır ve her deneme pahalıysa ya az dener ya da doğrulamadan "bitti" der.

Build sistemi kodun dışında yaşar. Xcode proje dosyası elle düzenlenmek için tasarlanmamış bir formattır, signing ayarları makineye ve hesaba bağlıdır. Gradle tarafında convention plugin'ler, version catalog ve build variant'lar görev adlarını belirler; testDebugUnitTest mi testStagingDebugUnitTest mi çalıştırılacağını repo'yu bilmeyen biri tahmin edemez.

Üretilmiş kod her yerdedir: OpenAPI istemcileri, kaynak erişimcileri, KSP ve Hilt çıktıları, protobuf sınıfları. Ajan bunları sıradan kaynak dosyası sanar, okur, bazen de düzenler. Bir sonraki build düzenlemeyi siler.

Bağlam sınırlıdır. Model büyük bir pencereye sahip olsa bile, o pencereye build logları ve ilgisiz altmış dosya dolduğunda asıl görev için kalan dikkat azalır. Kurallara uyum da bağlam kalabalıklaştıkça düşer.

En pahalısı da şudur: kurallar insanların kafasında yaşar. "Feature modülleri birbirini import etmez", "yeni ekranlar SwiftUI ile yazılır ama checkout hâlâ UIKit", "string'ler yalnızca tr ve en için elle girilir, gerisini çeviri ekibi doldurur". Bunlar review'da sözlü olarak aktarılır. Ajan review'a girmediği için hiçbirini bilmez.

Tek modülü komut satırından build ve test edebilmek

İlk yatırım, bir modülü tek başına, IDE açmadan, öngörülebilir bir komutla build ve test edebilmektir. Bu zaten insanlar için de istediğiniz bir şeydi; ajan yalnızca bunun eksikliğini görünür kılıyor.

iOS

Modülleriniz Swift package ise iki yol vardır. UIKit ya da SwiftUI'a bağlı olmayan, saf mantık içeren paketlerde swift build ve swift test en hızlı döngüdür:

swift test --package-path ios/Modules/FareAlertsDomain --filter PauseRuleTests

iOS SDK'sına bağlı paketlerde swift test host platformda (macOS) derlemeye çalışır ve çoğu zaman başarısız olur. Bu modüller için xcodebuild'i paketin kendi dizininde, simülatör hedefiyle çalıştırırız. Ajanın bu komutu her seferinde yeniden icat etmesini istemediğimiz için bir script'e sarıyoruz:

#!/usr/bin/env bash
# scripts/ios/test-module.sh <Modül> [TestSınıfı[/testMetodu]]
set -euo pipefail

MODULE="$1"
FILTER="${2:-}"
ROOT="$(git rev-parse --show-toplevel)"        # worktree içinde worktree kökünü verir
OUT="$ROOT/.agent-build/$MODULE"
SIM_ID="${AGENT_SIMULATOR_ID:?AGENT_SIMULATOR_ID tanımlı değil, scripts/ios/bootstrap.sh çalıştırın}"

mkdir -p "$OUT"
rm -rf "$OUT/result.xcresult"

ARGS=(
  test
  -scheme "$MODULE"
  -destination "platform=iOS Simulator,id=$SIM_ID"
  -derivedDataPath "$ROOT/.agent-build/DerivedData"
  -resultBundlePath "$OUT/result.xcresult"
  -quiet
)
if [[ -n "$FILTER" ]]; then
  ARGS+=("-only-testing:${MODULE}Tests/${FILTER}")
fi

cd "$ROOT/ios/Modules/$MODULE"
if xcodebuild "${ARGS[@]}" > "$OUT/xcodebuild.log" 2>&1; then
  echo "BAŞARILI: $MODULE"
else
  echo "BAŞARISIZ: $MODULE (tam log: $OUT/xcodebuild.log)"
  grep -E "error:" "$OUT/xcodebuild.log" | head -30 || true
  if [[ -d "$OUT/result.xcresult" ]]; then
    xcrun xcresulttool get test-results summary --path "$OUT/result.xcresult" --compact
  fi
  exit 1
fi

Script'in üç özelliği kasıtlı. Çıktıyı dosyaya yazıyor ve ajana yalnızca hataları gösteriyor, çünkü on bin satırlık build logu bağlamı dolduran ilk şeydir. DerivedData'yı repo'nun (ya da worktree'nin) içine alıyor, böylece paralel çalışan iki ajan birbirinin build'ini bozmuyor. Simülatörü kimliğiyle seçiyor, böylece "iPhone 17" adında iki cihaz olduğunda sonuç değişmiyor. Scheme adlarını tahmin etmeyin, xcodebuild -list ile kontrol edin; birden fazla ürünü olan paketlerde otomatik scheme adı farklı olabilir.

Paketleriniz macro ya da build tool plugin kullanıyorsa, başsız (headless) build'ler güven onayı ister. xcodebuild'in -skipMacroValidation ve -skipPackagePluginValidation bayrakları bunu atlar; aracın kendi yardım metni ikisini de güvenlik riski olarak işaretler. Bu bayrakları yalnızca sürümleri Package.resolved ile sabitlenmiş, kaynağına güvendiğiniz bağımlılıklar için ve bilerek açın.

Android

Gradle tarafında modül başına görevler zaten var; eksik olan çoğu zaman hangisinin doğru olduğunu söyleyen tek bir yer:

# Bir modülün birim testleri, tek sınıfa daraltılmış
./gradlew :feature:farealerts:testDebugUnitTest \
  --tests '*FareAlertsViewModelTest' --console=plain

# Aynı modül için lint (NewApi gibi kontroller burada yakalanır)
./gradlew :feature:farealerts:lintDebug --console=plain

# Yalnızca derleme doğrulaması
./gradlew :feature:farealerts:assembleDebug --console=plain

--tests filtresi Gradle'ın belgelediği bir seçenektir, joker karakter kabul eder ve birden fazla kez verilebilir. Configuration cache açıksa ikinci ve sonraki çağrılarda yapılandırma aşaması atlanır; ajan aynı komutu art arda onlarca kez çalıştırdığı için bu fark hissedilir. Emülatör gerektiren testler için Gradle Managed Devices tanımlayın: cihaz build dosyasında tarif edilir, görev adı <cihazAdı><Variant>AndroidTest kalıbını izler ve ajanın "hangi emülatör açık" sorusuyla uğraşması gerekmez.

İki platformda da kural aynı: komut tek satır olmalı, etkileşim istememeli, başarı ve başarısızlığı çıkış koduyla bildirmeli ve çıktısı kısa olmalı. Bu komutların yazılacağı yer bir sonraki bölümün konusu.

Kural dosyaları: kökte bir tane, her modülde bir tane

Ajanların oturum başında okuduğu talimat dosyaları için bugün fiilî ortak format AGENTS.md'dir: düz Markdown, zorunlu alanı yok, Linux Foundation altındaki Agentic AI Foundation tarafından yürütülüyor. Formatın monorepo kuralı basit: alt dizinlere de dosya koyarsınız, ajan üzerinde çalıştığı dosyaya en yakın olanı esas alır. Araçların yükleme davranışı ise aynı değil ve modül başına dosya tasarlarken bu farklar önem kazanıyor. Ekim 2026 itibarıyla dokümantasyonlarından doğruladığımız tablo:

Araç Okuduğu dosyalar Alt dizin dosyaları nasıl yüklenir
OpenAI Codex AGENTS.md, AGENTS.override.md Git kökünden çalışma dizinine kadar olan dosyaları sırayla birleştirir; toplamda varsayılan 32 KiB sınırı var
Claude Code CLAUDE.md; üst dizinlerde CLAUDE.md yoksa AGENTS.md (v2.1.277 ve sonrası); .claude/rules/*.md Çalışma dizini ve üstündekiler açılışta, alt dizindekiler ajan o dizinde dosya okuduğunda yüklenir
GitHub Copilot .github/copilot-instructions.md, .github/instructions/*.instructions.md (applyTo ile), AGENTS.md En yakın AGENTS.md önceliklidir
Cursor .cursor/rules/*.mdc (description, globs, alwaysApply), AGENTS.md Alt dizinlerdeki AGENTS.md dosyaları desteklenir
Gemini in Android Studio AGENTS.md Geçerli dizini ve üst dizinleri tarar; aynı dizinde GEMINI.md varsa o önceliklidir

Vardığımız karar: tek doğruluk kaynağı AGENTS.md olsun, kökte bir tane ve her modülde bir tane. Araca özel dosyaları ancak gerektiğinde ve yalnızca AGENTS.md'ye yönlendiren ince bir katman olarak ekleyin (Claude Code'da CLAUDE.md içine tek satır @AGENTS.md import'u bunu yapar). Aynı kuralı iki dosyada elle tutmaya başladığınız gün, biri eskir. Tablodan çıkan pratik bir sonuç daha var: Codex çalışma dizinine kadar inen dosyaları okuduğu için oturumu modül dizininde başlatmak, modül dosyasının yüklenmesini garanti eder.

Kök dosya kısa olmalı. Claude Code dokümantasyonu dosya başına 200 satırın altını hedeflemenizi, uzun dosyaların uyumu düşürdüğünü söyler; bizim gözlemimiz de aynı yönde. Kök dosyaya her modül için geçerli olan şeyler girer:

# Rota: ajanlar için kurallar

## Repo düzeni
- ios/Modules/<Ad>/         Her biri bir Swift package. Uygulama hedefi: ios/App
- android/feature/<ad>/     Feature modülleri. android/core/<ad>/ paylaşılan kod
- Her modülün kendi AGENTS.md dosyası var. Bir modülde çalışmadan önce onu oku.

## Komutlar
- iOS modül testi:      scripts/ios/test-module.sh <Modül> [TestSınıfı]
- Android modül testi:  ./gradlew :feature:<ad>:testDebugUnitTest --console=plain
- Android lint:         ./gradlew :feature:<ad>:lintDebug --console=plain
- Tüm uygulamayı build ETME. CI yapar.

## Sınırlar
- Görev bir modül adı verir. O modülün dışında dosya değiştirmen gerekiyorsa dur ve sor.
- *.xcodeproj, *.pbxproj, signing ayarları, Package.resolved, gradle/libs.versions.toml,
  build-logic/ : dokunma. Değişiklik gerekiyorsa PR açıklamasına yaz.
- Üretilmiş kod (ios/Modules/*/Generated, android/**/build/, *.pb.swift): düzenleme.
- Yeni üçüncü parti bağımlılık ekleme.

## Bitti demeden önce
- Değiştirdiğin modülün testlerini çalıştır ve komutun çıktısını PR açıklamasına ekle.
- Çalıştıramadığın bir şey varsa "çalıştırılamadı" diye açıkça yaz.

iOS feature modülü için örnek

Modül dosyası, o modülde çalışan birinin review'da en sık duyduğu şeyleri içerir:

# FareAlerts (iOS)

Fiyat alarmı listesi, oluşturma ve duraklatma ekranları.

## Bağımlılıklar
- İzinli: FareAlertsDomain, DesignSystem, Navigation, Analytics (arayüz)
- Yasak: başka herhangi bir Feature modülü, Networking (repository üzerinden git)

## Mimari
- Swift 6 dil modu. Bu paket defaultIsolation(MainActor.self) ile derlenir.
- Ekranlar SwiftUI, state @Observable ViewModel'de. ObservableObject kullanma.
- ViewModel'ler FareAlertRepository protokolünü init'ten alır. .shared yok.
- Örnek alınacak dosyalar: Sources/FareAlerts/List/FareAlertListViewModel.swift
  ve Tests/FareAlertsTests/FareAlertListViewModelTests.swift

## Platform
- Deployment target iOS 17. Daha yeni API kullanacaksan #available ile sar.
- @unchecked Sendable ve nonisolated(unsafe) ekleme. Gerekiyor gibiyse dur ve sor.

## Kaynaklar
- String'ler Resources/Localizable.xcstrings içinde. Yalnızca tr ve en gir,
  diğer dilleri boş bırak. Kodda String(localized:bundle: .module) kullan.
- Renk ve yazı tipi yalnızca DesignSystem'den.

## Test
- Swift Testing (@Test, #expect). Sahte repository: Tests/Support/FakeFareAlertRepository.swift
- Komut: scripts/ios/test-module.sh FareAlerts

Android feature modülü için örnek

# :feature:farealerts (Android)

## Bağımlılıklar
- İzinli: :core:designsystem, :core:navigation, :domain:farealerts, :core:analytics-api
- Yasak: diğer :feature:* modülleri, :core:network (repository üzerinden git)

## Mimari
- UI Compose. Ekran başına Route (ViewModel'i bağlar) ve Screen (state alır, önizlenebilir).
- ViewModel StateFlow<UiState> yayınlar. Route içinde collectAsStateWithLifecycle() kullan.
- DI: Hilt. Yeni binding'ler di/FareAlertsModule.kt içine. @Singleton ekleme.
- Örnek alınacak dosyalar: list/FareAlertListRoute.kt, list/FareAlertListViewModel.kt

## Platform
- minSdk 26. Daha yeni API için Build.VERSION.SDK_INT kontrolü şart; lint NewApi bunu yakalar.
- GlobalScope ve runBlocking kullanma. Uzun işler viewModelScope'ta.

## Kaynaklar
- Metinler src/main/res/values/strings.xml (en) ve values-tr. Diğer values-* dizinlerine dokunma.
- Kullanıcıya görünen hiçbir metin Kotlin koduna yazılmaz.
- Renk ve tipografi RotaTheme'den. Color(0x...) yazma.

## Test
- JUnit 5 + Turbine. Sahte repository: src/test/.../FakeFareAlertRepository.kt
- Komut: ./gradlew :feature:farealerts:testDebugUnitTest --console=plain
- Bitirmeden önce: ./gradlew :feature:farealerts:lintDebug --console=plain

Bu dosyalara girmeyenler de bir o kadar belirleyici. Mimari felsefe denemeleri, modülün tarihi, "temiz kod yazın" gibi doğrulanamayan öğütler ve linter'ın zaten yakaladığı biçim kuralları girmez. Ölçütümüz şu: her satır, yokluğunda ajanın yanlış yapacağı bir şeyi anlatmalı. Anlatmıyorsa yalnızca yer kaplıyor.

Bir uyarı daha: talimat dosyası bağlamdır, zorlama mekanizması değildir. Claude Code dokümantasyonu bunu açıkça söyler; diğer araçlarda da durum aynıdır. "Feature modülleri birbirini import etmez" kuralını dosyaya yazmak ajanın çoğu zaman uymasını sağlar. Her zaman uymasını sağlayan şey, aynı kuralı CI'da kıran bir kontroldür. Bunun nasıl kurulduğunu AI çağında mimari korkulukları yazısında anlatıyoruz.

Bağlam stratejisi: arayüzü ve örneği göster, grafiği sınır olarak kullan

Ajana "repo'yu anla" demek yerine neye bakacağını söylüyoruz. İyi bir görev tanımı üç şey içerir: modülün adı, uyulacak arayüz ve taklit edilecek bir örnek.

Modül: ios/Modules/FareAlerts
Görev: Listeye "alarmı duraklat" aksiyonu ekle.
Arayüz: FareAlertsDomain içindeki FareAlertRepository.pause(_:) zaten var, değiştirme.
Örnek: Silme aksiyonu aynı ekranda nasıl yapıldıysa (FareAlertListViewModel.delete) öyle yap.
Kabul: Başarılı duraklatmada satır "duraklatıldı" durumuna geçer; hata durumunda
       mevcut hata banner'ı gösterilir. İkisi için de test yaz.
Kapsam dışı: Android, analytics olayları, tasarım değişikliği.

"Örnek" satırı en çok iş gören satırdır. Ajanlar mevcut bir kalıbı kopyalamakta çok iyidir; kalıbı kendileri seçtiğinde ise repo'daki en eski ve en kötü örneği bulma olasılıkları hiç de düşük değildir.

Modüler bir kod tabanı burada ikinci kez karşılığını verir. Bağımlılık grafiği, ajanın okuması gereken alanı mekanik olarak sınırlar: FareAlerts yalnızca dört modüle bağlıysa, ajanın o dört modülün public arayüzünden başka bir şeye bakmasına gerek yoktur. Manifest bunu ajanın okuyabileceği biçimde zaten söyler:

// swift-tools-version: 6.2
// ios/Modules/FareAlerts/Package.swift
import PackageDescription

let package = Package(
    name: "FareAlerts",
    defaultLocalization: "en",
    platforms: [.iOS(.v17)],
    products: [.library(name: "FareAlerts", targets: ["FareAlerts"])],
    dependencies: [
        .package(path: "../FareAlertsDomain"),
        .package(path: "../DesignSystem"),
        .package(path: "../Navigation"),
        .package(path: "../AnalyticsInterface"),
    ],
    targets: [
        .target(
            name: "FareAlerts",
            dependencies: ["FareAlertsDomain", "DesignSystem", "Navigation", "AnalyticsInterface"],
            resources: [.process("Resources")],
            swiftSettings: [.defaultIsolation(MainActor.self)]
        ),
        .testTarget(name: "FareAlertsTests", dependencies: ["FareAlerts"]),
    ]
)
// android/feature/farealerts/build.gradle.kts
plugins {
    id("rota.android.feature")        // build-logic içindeki convention plugin
}

android {
    namespace = "app.rota.feature.farealerts"
}

dependencies {
    implementation(projects.core.designsystem)
    implementation(projects.core.navigation)
    implementation(projects.domain.farealerts)
    implementation(projects.core.analyticsApi)

    testImplementation(projects.core.testing)
}

Derleyici de aynı sınırı uygular: ajan yasak bir modülü import etmeye kalkarsa build kırılır ve hata mesajı ona nedenini söyler. Tek hedefli bir uygulamada böyle bir geri bildirim yoktur; her dosya her dosyayı görebilir ve ajan da görür. Modülerleşmenin maliyetini ayrı bir yazıda tartıştık; ajanlarla çalışmak o hesabın gelir tarafına yeni bir kalem ekliyor.

Görev boyutu için kuralımız: bir görev, bir modül. İki modüle dokunması gereken iş, önce arayüz değişikliği (insan tarafından ya da ayrı ve küçük bir PR olarak), sonra her modül için ayrı görev olarak bölünür.

Ajana göz ve el vermek, anahtarları vermemek

Ajanın işini doğrulayabilmesi için koddan fazlasını görmesi gerekir. Verdiklerimiz, önem sırasıyla şöyle.

Build ve test komutları, yukarıdaki script'ler üzerinden. Bu tek başına ajanı "yazdım, sanırım çalışıyor" seviyesinden "yazdım, testler geçti" seviyesine taşır.

Simülatör ve emülatör. iOS'ta xcrun simctl ile cihaz açmak, uygulamayı kurup başlatmak, deep link açmak ve ekran görüntüsü almak komut satırından mümkündür. Android'de aynı işleri adb yapar. Görüntü okuyabilen bir model için ekran görüntüsü, "boş durum metni iki satıra taşmış" gibi testlerin yakalamadığı hataları görmenin yoludur.

IDE'nin bildikleri. Apple, Xcode'un yeteneklerini dışarıdaki ajanlara MCP üzerinden açıyor: Intelligence ayarlarında "Allow external agents to use Xcode tools" seçeneğini açarsınız, ajanı xcrun mcpbridge komutuyla bağlarsınız. Apple'ın dokümantasyonuna göre ajanın çalışması için projenin Xcode'da açık olması gerekir; yani bu yol bir geliştiricinin makinesinde işe yarar, başsız bir CI koşucusunda xcodebuild script'leri hâlâ gereklidir. Android tarafında Google'ın Android CLI aracı (2026'da 1.0 oldu) benzer bir rol üstleniyor: android docs search ile güncel dokümantasyonu aratmak, android emulator ile emülatör yönetmek, android screen capture ile ekranı almak ve android studio alt komutlarıyla açık projede kullanım yeri bulmak gibi işleri tek bir komut satırı arayüzünde topluyor. İki aracın da alt komut ve araç listesi sürümden sürüme değişiyor; kendi kurulumunuzdaki listeye bakın.

Güncel dokümantasyon. Modelin eğitim verisi, kullandığınız SDK sürümünden eski olabilir. Dokümantasyon araması yapabilen bir araç (yukarıdakiler ya da MCP ile bağlanan bir doküman sunucusu), uydurma API ihtimalini azaltmanın en ucuz yoludur.

Vermediklerimiz daha kısa ama pazarlığa kapalı bir liste:

  • Dağıtım sertifikaları, provisioning profilleri, keystore dosyaları ve bunların parolaları.
  • App Store Connect ve Play Console API anahtarları, fastlane kimlik bilgileri.
  • Üretim ortamının API anahtarları ve gerçek kullanıcı verisi içeren her şey.
  • main ve release dallarına push yetkisi, PR birleştirme yetkisi.

Bunun somut bir gerekçesi var. Bulut üzerinde çalışan bir modelle, ajanın okuduğu her dosya ve çalıştırdığı her komutun çıktısı istemin parçası olarak sağlayıcıya gider; ne kadar süre saklandığı sözleşmenize ve plan ayarlarınıza bağlıdır. Ajanın okuyabildiği bir .env dosyası ya da build logunda görünen bir token bu yolculuğa dahildir. İkincisi, ajan talimatları yalnızca sizden almaz: okuduğu bir issue, bir bağımlılığın README'si ya da bir web sayfası da ona komut verir gibi yazılmış metin içerebilir. Elinde imzalama anahtarı ve push yetkisi olmayan bir ajanın kandırılması, olan bir ajanın kandırılmasından çok daha ucuza kapanır.

Araç tarafındaki izin ayarları bunun bir katmanıdır. Claude Code'da proje düzeyinde şöyle görünür (başka araçların kendi izin ve sandbox ayarları var, desen aynı):

{
  "permissions": {
    "allow": [
      "Bash(./scripts/ios/test-module.sh *)",
      "Bash(./gradlew *)",
      "Bash(xcrun simctl *)"
    ],
    "deny": [
      "Bash(git push *)",
      "Bash(./gradlew *publish*)",
      "Bash(fastlane *)",
      "Read(.env)",
      "Read(*.jks)",
      "Read(*.p12)",
      "Read(keystore.properties)"
    ]
  }
}

Aracın kendi dokümantasyonu, Bash kurallarının komut metnini eşleştirdiğini ve bir güvenlik sınırı sayılmaması gerektiğini söyler. Asıl sınırlar başka yerdedir: anahtarlar ajanın çalıştığı makinede hiç bulunmaz, imzalama yalnızca CI'da yapılır, main dalı sunucu tarafında korunur ve ajanın kullandığı token yalnızca kendi dalına yazabilir.

Ajana verilecek işler, verilmeyecek işler

İş tipi Neden uyar ya da uymaz Karar
Mekanik göç (ObservableObject'ten @Observable'a, LiveData'dan StateFlow'a, modül modül) Kalıp net, her modül bağımsız doğrulanır Verin, modül başına bir PR
Mevcut koda test yazmak Arayüz ve örnek test hazır, sonuç çalıştırılarak doğrulanır Verin, testin gerçekten bir şey sınadığını review'da kontrol edin
Yeni bir iç API'yi modüllere yaymak Bir modülde insan yapar, kalanı aynı kalıbın tekrarıdır Verin, ilk örneği siz yazın
Bug'ı yeniden üretmek "Bu crash'i gösteren başarısız bir test yaz" doğrulanabilir bir hedeftir Verin, düzeltmeyi ayrı adım yapın
Tek modülde, tasarımı belli küçük özellik Sınır ve kabul kriteri net Verin
Modüller arası yeniden tasarım (navigation'ı değiştirmek, DI çerçevesi geçişi) Karar çok, doğrulama ancak bütün uygulamada mümkün Tasarımı insan yapsın, ajan yalnızca mekanik adımları alsın
Belirsiz ticket ("checkout yavaş", "kodu temizle") Bitti ölçütü yok, ajan kapsamı kendi belirler Vermeyin; önce ölçün ve ticket'ı daraltın
Signing, release, mağaza yapılandırması Hata pahalı, yetki gerektirir Vermeyin

Tablonun ortak ölçütü, işin makine tarafından doğrulanabilir bir bitiş çizgisinin olması. "Testler geçiyor ve lint temiz" bir bitiş çizgisidir. "Daha iyi oldu" bir bitiş çizgisi olmadığı için ajan onu kendi lehine yorumlar.

Paralel çalıştırmak: worktree, hakem olarak CI ve review yükü

İşler modül boyutunda ve bağımsız olduğunda, birden fazla ajanı aynı anda çalıştırmak mümkündür. Her ajana kendi çalışma kopyasını git worktree ile veriyoruz:

git worktree add ../rota-wt-farealerts -b agent/farealerts-pause
git worktree add ../rota-wt-search     -b agent/search-observable

# Her worktree için ayrı simülatör
xcrun simctl create "agent-farealerts" "iPhone 17"   # çıktısı cihaz kimliğidir
export AGENT_SIMULATOR_ID=<yukarıdaki kimlik>

Mobilde worktree tek başına yetmez; paylaşılan üç kaynak daha var. DerivedData ortak kalırsa iki build birbirini bozar (script'imiz bunu worktree içine alarak çözüyor). Aynı simülatöre iki ajan uygulama kurarsa testler birbirinin durumunu görür; her ajana simctl create ile kendi cihazını açın. Android'de her emülatör ayrı bir seri numarasıyla görünür ve adb hedefini ANDROID_SERIAL ortam değişkeninden alır. Dördüncü kaynak makinenin kendisidir: aynı anda çalışan her Xcode build'i ve her Gradle daemon'ı ciddi bellek ister. Kaç ajanın paralel çalışabileceğini tahmin etmeyin; iki, üç ve dört paralel modül build'inin süresini kendi makinenizde ölçün ve sürenin bozulduğu noktanın bir altında kalın.

Hakem CI'dır. Ajanın "testler geçti" beyanı bir iddiadır; PR üzerindeki yeşil işaret kanıttır. CI'nın ajan PR'larına insan PR'larından farklı davranmaması gerekir: aynı testler, aynı lint, aynı mimari kontrolleri.

Asıl darboğaz ise review'dur. Beş ajan günde yirmi PR açabilir; ekibiniz yirmi PR'ı dikkatle okuyamaz. Paralelliği makinenin kapasitesine göre ayarlamak yanlıştır, review kapasitesine göre ayarlayın. PR'ları küçük ve tek modüllük tutmak burada da işe yarar, çünkü küçük bir diff'i okumak mümkündür. Ajan çıktısını review etmenin kendine özgü taraflarını AI ile code review yazısında ele alıyoruz.

Paralel ajan sayısının üst sınırı review kapasitenizdir.

Mobilde sık gördüğümüz hata türleri

Proje dosyalarını düzenlemek

project.pbxproj içinde her dosya birkaç ayrı yerde, benzersiz kimliklerle geçer. Ajan bunu elle düzenlediğinde sonuç ya açılmayan bir proje ya da başka dalla birleşmeyen bir diff olur. Çözümler etkililik sırasına göre: modülleri Swift package olarak tutmak (dosya eklemek manifest değişikliği bile gerektirmez), Xcode 16 ile gelen klasör eşitlemeli grupları kullanmak, proje dosyasını bir üreteçle (Tuist, XcodeGen) üretip repo'ya hiç koymamak. Android'de karşılığı libs.versions.toml ve build-logic'tir: ajan derleme hatasını "çözmek" için bir kütüphanenin sürümünü yükseltir ve bütün uygulamayı etkiler. İkisi de kök dosyadaki "dokunma" listesinde.

Yanlış OS ya da SDK sürümü için API uydurmak

İki biçimi var. Birincisinde API hiç yoktur; derleyici yakalar, ajan düzeltir, kayıp küçüktür. İkincisinde API gerçektir ama deployment target'ınızdan yenidir. iOS'ta derleyici bunu availability hatası olarak yakalar. Android'de derleme geçer; hatayı lint'in NewApi kontrolü yakalar, o da yalnızca lint çalıştırılırsa. Modül dosyasındaki "bitirmeden önce lint çalıştır" satırının sebebi budur. Ters yönü de görüyoruz: ajan, eğitim verisinde çok örneği olduğu için eski API'yi (ObservableObject, LiveData) yeni koda yazar. Bunu derleyici yakalamaz; modül dosyasındaki "örnek alınacak dosya" satırı yakalar.

Main actor ve yaşam döngüsü kurallarını yok saymak

Aşağıdaki kod ajanların sık ürettiği türden. Derlenmesi için @unchecked Sendable eklenmiş olması, review'da ilk bakılacak işarettir:

// Ajanın ürettiği: derleniyor, testi de geçiyor
final class FareAlertsViewModel: ObservableObject, @unchecked Sendable {
    @Published var alerts: [FareAlert] = []
    private let repository: any FareAlertRepository

    init(repository: any FareAlertRepository) { self.repository = repository }

    func pause(_ alert: FareAlert) {
        Task.detached {
            try? await self.repository.pause(alert.id)
            let updated = (try? await self.repository.alerts()) ?? []
            DispatchQueue.main.async { self.alerts = updated }
        }
    }
}

Hata yutuluyor, izolasyon derleyiciden gizleniyor, test edilebilir bir async yüzey yok. Modülün kuralına uyan hâli daha kısadır:

@MainActor
@Observable
final class FareAlertsViewModel {
    private(set) var alerts: [FareAlert] = []
    private(set) var errorMessage: String?
    private let repository: any FareAlertRepository

    init(repository: any FareAlertRepository) { self.repository = repository }

    func pause(_ alert: FareAlert) async {
        do {
            try await repository.pause(alert.id)
            alerts = try await repository.alerts()
        } catch {
            errorMessage = String(localized: "fareAlerts.pause.failed", bundle: .module)
        }
    }
}

Android'de aynı ailenin üyesi yaşam döngüsüdür:

// Ajanın ürettiği: derleniyor, önizlemede doğru görünüyor
@Composable
fun FareAlertsRoute(viewModel: FareAlertsViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsState()

    LaunchedEffect(Unit) { viewModel.load() }

    if (state.alerts.isEmpty()) {
        Text("Henüz fiyat alarmınız yok", color = Color(0xFF6B7280))
    } else {
        FareAlertList(state.alerts, onPause = viewModel::pause)
    }
}

collectAsState() uygulama arka plandayken de toplamaya devam eder, LaunchedEffect(Unit) ekrana her dönüşte yeniden yükler, metin ve renk koda gömülüdür. Modül kurallarına uyan hâli:

@Composable
fun FareAlertsRoute(viewModel: FareAlertsViewModel = hiltViewModel()) {
    // Yükleme ViewModel'de: state, stateIn(WhileSubscribed) ile üretilir
    val state by viewModel.state.collectAsStateWithLifecycle()

    if (state.alerts.isEmpty()) {
        RotaEmptyState(text = stringResource(R.string.fare_alerts_empty))
    } else {
        FareAlertList(state.alerts, onPause = viewModel::pause)
    }
}

İki örnekte de ilk sürüm derlenir ve birim testini geçer. Bu yüzden bu kuralları modül dosyasına yazıyoruz ve mümkün olanları lint kuralına çeviriyoruz.

Kaynak ve yerelleştirme dosyaları

String Catalog (.xcstrings) tek bir büyük JSON dosyasıdır. Ajan bir anahtar eklemek için dosyayı baştan yazarsa çeviri durumları kaybolabilir ve paralel dallarla çakışma kaçınılmaz olur. İkinci risk iyi niyetlidir: ajan on dört dilin hepsini kendisi çevirir ve PR'da kimse Lehçe metni kontrol etmez. Kuralımız yalnızca kaynak dilleri girmesi, gerisini çeviri sürecine bırakmasıdır. SPM modüllerinde bir tuzak daha var: bundle: .module unutulursa anahtar uygulama bundle'ında aranır ve kullanıcı ekranda ham anahtarı görür; birim testleri bunu yakalamaz, ekran görüntüsü yakalar. Android'de karşılığı, values-* dizinlerinden yalnızca birine eklenen ya da doğrudan Kotlin koduna yazılan metindir.

Nerede durmalı

Bu hazırlığın tamamı her projeye gerekmez. Tek hedefli, beş ekranlı bir uygulamada kökte kısa bir AGENTS.md ve çalışan bir test komutu yeterlidir; modül başına dosya, worktree düzeni ve simülatör havuzu orada bakım yüküdür. Tersi durumda, modül sınırları olmayan büyük bir monolitte kural dosyası yazmak sınırlı fayda sağlar, çünkü ajanın okuyacağı alanı daraltacak bir yapı yoktur; orada önce modülerleşmeye yatırım yapın. Kural dosyalarının kendisi de bakım ister: bir kuralı review'da üçüncü kez söylüyorsanız dosyaya ekleyin, bir kural artık lint tarafından zorlanıyorsa dosyadan silin.

Hazırlığın işe yaradığını ölçmek için model karşılaştırmalarına bakmayın, kendi PR'larınıza bakın: ajan PR'larının kaçı ilk CI çalışmasında yeşil, kaçı görev tanımındaki modülün dışına taşmış, kaçı review'da "bunu burada böyle yapmıyoruz" yorumu almış. Bu üç sayı iyileşiyorsa repo ajana hazır hâle geliyor demektir.

Hazırlık kontrol listesi

  • Her modül, IDE açmadan tek bir komutla build ve test edilebiliyor; komut etkileşim istemiyor ve çıktısı kısa.
  • iOS: test script'i DerivedData'yı çalışma kopyasının içine alıyor ve simülatörü kimlikle seçiyor.
  • Android: modül başına birim testi ve lint görevleri belli; emülatör gerektiren testler için managed device tanımlı.
  • Kökte 200 satırın altında bir AGENTS.md var: repo düzeni, komutlar, dokunulmayacak dosyalar.
  • Sık değişen her modülde kendi AGENTS.md dosyası var: izinli bağımlılıklar, örnek alınacak dosyalar, platform sürümü, kaynak kuralları, test komutu.
  • Kullandığınız her aracın bu dosyaları nasıl yüklediği kendi dokümantasyonundan kontrol edildi; aynı kural iki dosyada elle tutulmuyor.
  • Dosyadaki mimari kurallarının en kritik olanları CI'da da zorlanıyor.
  • Görev şablonu modül, arayüz, örnek, kabul kriteri ve kapsam dışını içeriyor; bir görev bir modüle dokunuyor.
  • Ajanın çalıştığı ortamda signing anahtarı, mağaza kimlik bilgisi ve üretim anahtarı yok; main sunucu tarafında korunuyor.
  • Proje dosyaları, version catalog ve üretilmiş kod "dokunma" listesinde.
  • Paralel çalışmada her ajanın kendi worktree'si, kendi DerivedData'sı ve kendi simülatörü ya da emülatörü var.
  • Paralel ajan sayısı review kapasitesine göre sınırlandı.
  • İlk CI çalışmasında yeşil olan, modül dışına taşan ve kural yorumu alan ajan PR'larının sayısı takip ediliyor.

Bu konuda desteğe mi ihtiyacınız var?

Uygulamanızda bu kontrollerin doğru kurulup kurulmadığını birlikte inceleyelim. Güvenlik denetimi ya da danışmanlıkla buradayız.

Yorumlar ve sorular

Katılmadığınız bir nokta ya da projenizde takıldığınız bir yer mi var? Yazın, konuşalım.