A new thread for the same task means the previous try failed, so old branches, PRs, and conclusions should not be trusted by default. Co-authored-by: Cursor <cursoragent@cursor.com>
8.3 KiB
AGENTS.md
This repo rebuilds a signed Android APK (Soul2 / Soul2⁺ Browser) from Apktool smali sources under app/. There is no Gradle app module, no npm/pip app stack, and no long-running service. The practical check for most changes is ./scripts/build.sh plus apksigner verify on the APK in dist/.
sources/java/ and sources/resources/ are JADX output for reading and searching. The buildable truth is app/ (smali + res). Some injected helpers live as Java under sources/warp-runtime/, sources/ocr-runtime/, etc., and are compiled to dex by scripts/compile-*.sh then merged by the build.
Package ID and CI
- Tracked
app/sources use the testing application ID (net.kaki87.soul2.testing, Soul2⁺ Browser). Feature-branch work should leave that alone. - Stable releases (
net.kaki87.soul2, Soul2 Browser) are produced only when CI builds a tag whose commit is onmainand setsSOUL_PACKAGE_ID=net.kaki87.soul2. Plain pushes tomainskip the APK job. - Do not hand-edit package names across the tree. Use
scripts/set-package-id.pywhen a rewrite is required, and runpython3 scripts/set-package-id.py --self-testafter changing that script. - Smali must reference
Lnet/kaki87/soul2/testing/R$string(or otherR$*) in the tracked tree. A leftover.../soul2/testing/testing/R$stringor aset-package-iddouble rewrite has crashed Copy URL and similar paths before.
Worktrees and PRs
Feature work normally lands in a git worktree on its own branch, not on the primary main checkout. Parallel agent sessions each get their own worktree so builds, dirty trees, and emulator runs do not collide.
- If this thread is a retry of a mission that was tried before, treat earlier attempts as failed. Discard their branches, PRs, patches, and conclusions unless the user points at something specific to keep. A new thread exists because the previous one did not succeed.
- Always create and use a feature branch unless the user explicitly says not to. Prefer names like
fix/...orfeature/...(short and descriptive). Hosts may also createt3code/...orcursor/...branches; keep using that branch if the session already checked it out. - Do not commit or push to
mainunless the user explicitly asks. Open a PR intomaininstead. - Always push the branch after commits. Always open a PR into
mainif one does not exist yet for that branch; if a PR already exists, push updates to it. - One concern per PR. After a PR merges, start the next change from an updated
main(new branch/worktree), rather than piling unrelated commits onto the old branch. - Before opening or updating a PR, rebase or merge
mainif the branch has drifted. Resource-ID and smali conflicts are common after adjacent features land; resolve them in the worktree, then push. - Non-
mainbranch pushes get a CI APK build. Use that artifact (or a local./scripts/build.sh) for device checks. Keep the PR focused: no generatedbuild_date.xml/soul2_info.xml, no accidentalbundled-warp/lib/noise, no unrelated reformatting. - PR summaries should say what changed and how to verify (build and/or a short UI path). Do not merge unless asked.
Build
- Run
./scripts/build.sh(details in the README Development notes). Needs Java 21+,curl,zip/unzip,keytool, and Android SDK build-tools withapksigneronPATH(or viaANDROID_HOME/ANDROID_SDK_ROOT). - Output: signed APK under
dist/. Signature schemes v1+v2+v3 are required (targetSdk 36). apktoolis downloaded intotools/by the build script (gitignored).app/res/values/build_date.xmlandapp/res/values/soul2_info.xmlare generated at build time and gitignored. INFO screen text comes from the README<!-- BEGIN INFO -->block viascripts/prepare-info-string.py. Do not commit those XML files.bundled-warp/lib/is gitignored for normalgit addbut kept in the repo history; CI refreshes it withgit add -fwhenbundled-warp/soulamz.inputschanges. Local soulamz rebuilds should not dirty commits.- OCR/WARP dex prebuilts live under
prebuilts/(tracked; CI updates whenprebuilts/tooling.inputschanges). - There is no separate unit-test suite. Prefer a green local (or CI) APK build over guessing.
Editing smali and resources
New strings and resource IDs
When adding a string (or other resource) used from smali:
- Add the entry in
app/res/values/strings.xml(English only here). - Allocate a new ID in
app/res/values/public.xmland the matching field inapp/smali_classes3/com/mycompany/app/soulbrowser/R$string.smali. - Keep
sources/java/.../R.javain sync if you touch it for readability, but apktool does not build from that tree.
ID collisions break main after merge even when a PR build was green on an older base. Never reuse an ID that already exists on main. After Eruda, reload_saved_tabs reused dev_tools's 0x7f12059d and apktool failed only once both landed together. Scan the highest existing custom IDs before picking the next one. scripts/sync-public-xml.py can add missing public.xml rows from R$*.smali; it does not invent new IDs for you.
Copy an existing pattern
Before inventing control flow, find the closest shipped feature and mirror it:
| Kind of change | Good anchors |
|---|---|
| Main-menu item | MainConst.smali, MenuListAdapter*.smali, Eruda/DevTools (DevToolsHelper.smali) |
| Settings row | The target Setting*.smali + SettingListAdapter*.smali; match group corner modes (top of group ≠ middle row) |
| Link/image long-press | DialogUrlLink.smali, preview hooks in DialogWebView*.smali / WebViewActivity*.smali |
| Prefs | Existing Pref*.smali getters/setters; do not invent fields on foreign classes |
| Tab list / multi-select | WebTabAdapter.smali, DialogTabMain.smali |
Settings list crashes in the WARP work came from writing final adapter fields or assuming ViewHolder members that are not in this decompile. Read the class you call into. If JADX and smali disagree on a field, trust the smali.
Smali mechanics that have burned builds
- Keep
.locals/ register widths honest after inserts. Wide values (long/double) take two registers. - Do not blindly rewrite or "refresh" unrelated
sparse-switch/packed-switchtables. A package-id helper once remapped colliding:sswitch_Nlabels file-wide and corrupted Jsoup/SVGParser, which made Image viewer show "No images" (VerifyError). Scope switch edits to the method you mean. - Anonymous classes are separate
Foo$N.smalifiles; wire listeners there the same way neighboring features do. - After process-death / intent bugs, clear consumed launch extras once handled (
WebViewActivityexternal-link reopen).
i18n
English strings are owned in this repo. Other locales come from soul-browser-i18n at build time (scripts/sync-i18n.py). Do not commit downloaded locale trees. CI can push English to the i18n repo from main via scripts/push-i18n-english.py.
Emulator and device smoke
Native libs in the APK are ARM-only. On an x86_64 emulator you need an image that advertises ARM ABIs via NDK translation, e.g. system-images;android-30;google_apis;x86_64. API 33 google_apis x86_64 has failed install with INSTALL_FAILED_NO_MATCHING_ABIS.
./scripts/run-emulator.sh creates the soul_test AVD if needed and starts a headless emulator. It forces -accel off (TCG) for nested-VM hosts where KVM misbehaves; on a normal desktop with working KVM you can boot the same AVD with acceleration enabled instead. Start adb first. Avoid pkill -f emulator (it can kill the calling shell); prefer killall qemu-system-x86_64-headless when that is the process in use.
Smoke after boot (local/feature APKs use the testing id):
adb wait-for-device
adb shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 2; done'
adb install -r -g dist/soul-browser-*.apk
adb shell am start -a android.intent.action.VIEW -d 'https://example.com/' \
-n net.kaki87.soul2.testing/com.mycompany.app.web.WebLauncher
# First launch shows onboarding; tap Start (id/splash_apply_view)
Stable-tag APKs use net.kaki87.soul2 in the component name. When UI is not part of the task, skip the emulator and rely on build + apksigner verify. Physical devices via ADB are fine when the user already has one connected; prefer CI or local dist/ APKs the user asked you to use.