# AGENTS.md Instructions for coding agents working in this repository. ## Code style - Do what you believe is right. Make the change complete and correct, not the smallest possible diff. If a fix calls for refactoring, renaming, or touching multiple files, do it. - Match the patterns and conventions already in the surrounding code. - Do not add copyright or license headers unless asked. ## Project layout | Path | Role | |------|------| | `app/` | Full builder host: editor UI, export pipeline, runtimes, preview. | | `shell/` | Runtime template. Built to `app/src/main/assets/template/webview_shell.apk` via `:shell:assembleRelease` + `:app:syncShellTemplateApk`. | | `clone-host/` | Host-side APK clone / identity reshape support library. Its DEX asset generation (`syncCloneHostDex`) is deliberately disabled (`enabled = false`, AV false-positive mitigation, e0d2d4d6) — `AppCloner` runs fail-soft without the asset. | | `modules/` | Module Market catalog (`registry.json` + per-module folders). | | `docs/` | VitePress documentation site (guide / developer / extensions, EN + ZH), published to https://shiaho777.github.io/web-to-app/ by `.github/workflows/docs-deploy.yml`. Site URL paths map 1:1 to files under `docs/` (`/zh/...` → `docs/zh/...`). | | `scripts/` | Build helpers and gates (`check_config_field_drift.py`). | Runtime Kotlin is authored under `app/` and synced into `shell` by `syncShellRuntimeSources`. Edit the `app/` source once; do not permanently fork copies under shell. User-facing product docs: `README.md`, `.github/docs/README_CN.md`, `.github/CONTRIBUTING.md`, `modules/README.md`. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: `docs/`, deployed by `.github/workflows/docs-deploy.yml`). ## How the main pieces connect ```text Editor (Compose screens in app/) ↔ data models (WebApp, configs) ↔ export factory (ApkConfig / ApkConfigJsonFactory) ↔ ApkBuilder / ApkBuildCache → signed generated APK app/ sources → syncShellRuntimeSources → shell DEX → webview_shell.apk (template) Generated APK runtime DruksCodeApplication → ShellModeManager → load assets JSON config → WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress) ``` Mental model: - **Host preview** runs `:app` with all classes on the main classpath. - **Generated APK** runs the shell template classes (full runtime synced from `app/`), reading config from assets JSON via `ShellModeManager`. - A flag in the editor is useless at export unless it flows through **model → ApkConfig JSON → shell config → runtime code**. ## i18n - Host UI strings live in `app/src/main/java/br/com/drukstech/codeapp/core/i18n/Strings.kt` (split across `Strings` / `StringsA` … `StringsE`). - **All** user-visible strings must be inline `when (Strings.lang)` blocks covering all 10 languages: Chinese, English, Arabic, Portuguese, Spanish, French, German, Russian, Japanese, Korean. `when(lang)` blocks may never use `else ->` — `AppStringsResourceConsistencyTest` and `StringsKtTranslationParityTest` enforce this. - **Never** load user-visible text via `context.getString(R.string.*)` / `stringResource(R.string.*)`. `res/values/strings.xml` holds only `translatable="false"` resources (e.g. `app_name`) and no locale `values-*/` directories exist, so a localized resource string could never cover the 10 languages and would silently fall back to the default `values/` (Chinese). Use `Strings.xxx` (or `Strings.funName(arg)` for parameterised strings — see `linuxEnvInstalledToast(name)` for the pattern). Tests gate this: `kotlin source never references R string for user-visible text`, plus `values strings xml only holds non-localised resources` and `no locale values dirs or grouped app strings files exist` which block resurrecting resource-based strings. - `R.string` is reserved for `translatable="false"` non-localised resources only (e.g. `app_name`). - Prefer adding properties on the existing split objects; match surrounding style. ## Android and packaging constraints - Generated apps keep a low `targetSdk` (28) on the shell path because they rely on on-device fork+exec runtimes. Do not raise shell targetSdk casually. - The host app targets SDK 35 (antivirus reputation); SELinux W^X therefore blocks host-side exec of downloaded runtimes. `RuntimeExecPolicy` (`core/linux`) gates those previews by probing the installed targetSdk — generated APKs (always 28) pass unconditionally. Node preview (JNI via native libs) is unaffected. - Launch runtime / toolchain processes through `HostProcessLauncher` (`core/linux`): plain `ProcessBuilder` fork+exec where the platform allows it (generated APKs, targetSdk 28), the user-mode static exec loader (`StaticExecProcess`, memfd-based) on W^X hosts (targetSdk ≥ 29), or a degraded error when no static exec bridge is available. A raw `Runtime.exec` on a downloaded binary crashes the host app on W^X devices (#795). - Avoid new third-party dependencies unless strongly justified (`app/build.gradle.kts` / `shell/build.gradle.kts`). Prefer platform APIs and existing modules. - Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default. - Foreground services and notification helpers must use `SafeNotificationChannels` (or equivalent fail-soft create). Channel creation failures must not crash FGS startup. - **One shell template:** `webview_shell.apk` from `:shell` release. Do not introduce a second template APK. - Export incremental rebuild lives in `app/.../apkbuilder` (`ApkBuildCache` + `ApkBuilder`): - Modes: `FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`. - Template / entry identities must be **content-stable** (no mtime-based keys). - Encrypted builds always force a full rebuild. - Do not feed signed or renamed APKs back into full `modifyApk` as templates. - Port coordination: `PortManager` + `PortConflictMode` (`REASSIGN` / `AUTO_KILL` / `ALERT`) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop. - Local server / Linux env DNS: fork+exec runtimes (Node / PHP / Python / Go / WordPress / Linux env) should wire through `LocalDnsBridgeProxy` when they need host DNS/proxy bridging. - Large runtime downloads use `NetworkModule.downloadClient` (extended timeouts), not the default short-lived client. - HTML / FRONTEND packaged shells need file-scheme access via `ShellWebViewConfig` (`allowFileAccess` / local-file detection). Do not regress pure file-based HTML loads. - Node.js export must embed `libnode_bridge.so`, `libnode.so` (16KB-aligned), and `libc++_shared.so` as native libs. Go export must embed `libgo_exec_loader.so`. - Gradle custom tasks (`syncCloneHostDex`, etc.) must be configuration-cache safe: capture `File`/`Provider` values at configuration time, do not reference `Project`/`android.sdkDirectory` inside task closures. ## Workflow - Do not commit secrets, `local.properties`, keystores, or IDE/cache junk. - Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent). - When changing export or shell packaging, rebuild the template you touched. - When changing config fields, run `checkConfigFieldDrift` to catch model ↔ shell config name drift. ### Delivery (Issue + PR + CI) Default target: [shiaho777/drukscode](https://github.com/shiaho777/web-to-app). Prefer a pull request over direct pushes to `main` when delivering code. Human-facing wording of the same loop lives in [CONTRIBUTING.md](.github/CONTRIBUTING.md); keep those docs in sync when this process changes. **Language (required):** GitHub **Issues and PRs must be written in English** — titles, bodies, labels text you author, and delivery comments on the Issue/PR. Local chat with the user may be Chinese or any language; do not copy that language into Issue/PR text. When the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Do not close the Issue until the PR is merged and CI is green. --- ## Shell template and runtime sync ### Dual runtime (preview vs export) | | Host `:app` | Generated APK | |--|-------------|---------------| | DEX | All `app/src` classes | Shell sync include−exclude (full runtime set) | | Config | Editor / in-memory models | Assets JSON via `ShellModeManager` | | Template | Not used | `app/src/main/assets/template/webview_shell.apk` | **Preview ≠ export** unless both paths stay valid: 1. Host still has the real implementation. 2. Shell-synced code reads the config field from assets JSON at runtime. 3. The field name in `ApkConfig` JSON matches the `@SerializedName` in `ShellModeManager` (Gson silently drops mismatches). Most common failure: preview works; exported APK silently skips the feature because a config field name drifted between the export factory and the shell config class. ### Where each concern is edited | Concern | Path | |---------|------| | What enters shell | `shell/build.gradle.kts` → `syncShellRuntimeSources` include/exclude | | Shell template build | `:shell:assembleRelease` + `:app:syncShellTemplateApk` | | Template output | `app/src/main/assets/template/webview_shell.apk` | | Config → shell JSON | `app/.../apkbuilder/ApkConfigJsonFactory.kt` | | Shell config types | `app/.../core/shell/ShellModeManager.kt` | | Runtime WebView config | `app/.../ui/shell/ShellWebViewConfig.kt` | | Config drift gate | `scripts/check_config_field_drift.py` → `:app:checkConfigFieldDrift` | | Shell minify policy | `shell/proguard-rules.pro` | --- ## Common change recipes These are the default approaches for everyday work. Follow the chain end-to-end; stopping at UI or host-only code is how preview and export diverge. ### 1. Add or change a host UI string 1. Add the property in the correct `Strings*` split with all 10 languages. 2. Reference it from Compose/UI the same way neighbors do. ### 2. Add an editor setting that must affect the generated APK Trace and update **all** of: 1. Model (`WebApp` / nested config) and editor UI binding 2. Export mapping (`ApkBuilder` / `ApkConfig` / `ApkConfigJsonFactory`) 3. Shell config types (`ShellModeManager` / shell config data classes) if runtime reads them 4. Runtime use site in shell-synced code 5. Unit tests for export wiring when flags change 6. **Coverage test update (REQUIRED).** When adding a new Boolean field to `WebViewConfig`, you MUST add it to `flipAllBooleans()` in `WebViewConfigBooleanCoverageTest.kt`. The test `flipAllBooleans covers every declared Boolean field` uses reflection to verify that every declared Boolean field is listed — if you forget, CI fails with a message naming the missing field. For non-Boolean fields, add a spot-check to `key non-Boolean WebViewConfig fields survive the export round-trip`. This is the safety net that catches "preview works, export broken" before it ships. 7. **Shell UI parity (REQUIRED for per-type player screens).** Gallery / media / splash-style app types render through DIFFERENT composables in preview (`ui/gallery/`, `ui/media/`) vs generated APKs (`ui/shell/`). A flag that flows end-to-end still does nothing if the shell screen never reads it (gallery thumbnail bar shipped config + assets correctly and still rendered nothing, #781). `ShellUiParityTest.kt` enforces this: every model field read by host runtime UI must also be read by shell runtime UI. If you add a field and read it in a host player, the test fails naming the field until you port the behavior to shell or add an explicit `allow` entry with a reason. When adding a new per-type player pair, extend its `pairs` list the same way. Missing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads. ### 3. Change shell runtime behavior used by every generated app 1. Edit the source under `app/` (shared runtime). 2. Confirm the file is included by `syncShellRuntimeSources`. 3. Rebuild shell template if you need to validate packaging. 4. Keep changes surgical; shell has a low targetSdk and a thin dependency set. 5. If you touch FGS / notification channel creation, fail soft via `SafeNotificationChannels`. ### 4. Add a host-only feature (editor, market, tooling) 1. Keep implementation under host-only packages (`core/apkbuilder`, host screens, sample/market, `core/host`, …). 2. Do not pull host-only deps into `shell/build.gradle.kts`. ### 5. Touch APK export or incremental rebuild 1. Prefer `ApkBuildCache` / content hashes over timestamps. 2. Encrypted builds stay full rebuild. 3. Do not use signed outputs as templates. 4. If template bytes change, ensure cache keys invalidate correctly. ### 6. Change notifications / engines / network hardening 1. Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM). 2. Do not add OEM push SDKs by default. 3. FGS channel create paths must tolerate OEM/channel failures. ### 7. Module Market / `modules/` 1. Follow `modules/README.md` catalog layout (`registry.json` + module folders). 2. Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs. ### 8. Fix "works in preview, broken after export" Checklist in order: 1. Did shell config JSON actually contain the field at runtime? 2. Does the field name in `ApkConfig` JSON match the `@SerializedName` in shell config? Run `checkConfigFieldDrift`. 3. Is the runtime use site shell-synced (not host-only)? 4. For adblock: confirm `adBlockEnabled` mapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state. 5. Rebuild template after sync changes (stale template is a frequent miss). ### 9. Local server runtime / download path 1. Allocate ports through `PortManager` with the configured conflict policy; implement real stop handlers. 2. Wire fork+exec processes into `LocalDnsBridgeProxy` when they need host DNS/proxy env. 3. Use `NetworkModule.downloadClient` for large dependency / engine / runtime downloads. 4. Launch processes through `HostProcessLauncher` (`core/linux`) so W^X hosts (targetSdk ≥ 29) degrade to the user-mode static exec loader instead of crashing. ### 10. Node.js / Go export 1. Node.js: ensure `injectNodeJsNativeLibs` embeds `libnode_bridge.so` + `libnode.so` (16KB-aligned via `ElfAligner16k`) + `libc++_shared.so`. Node binary resolution prefers `nativeLibraryDir`, falls back to download cache. 2. Go: ensure `injectGoExecLoaderNativeLib` embeds `libgo_exec_loader.so`. 3. Go host-side builds never run the multi-process `go build` driver: `GoDirectBuilder` replays `compile`/`asm`/`link` as single-shot processes through `HostProcessLauncher` (user-mode static exec loader on W^X hosts), with a content-keyed persistent package archive cache (#796). 4. `NodeService` runs in a dedicated `:nodejs` OS process so V8 lifecycle is isolated from the host. ### 11. Change a feature that has an Agent tool The in-app Agent exposes up to 57 tools — 52 base + 2 plan-mode + 3 imagery (imagery only load with an image-capable model; see `ToolRegistryFactory.build()`) — that wrap host service classes. When you change a feature, trace the tool chain: ```text LLM response (tool_calls) → AgentEngine.executeToolCall() → PermissionPrompter (write tools ask user; read-only run silently) → Tool.execute(args, ctx) → Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …) → ToolResult → back to LLM ``` Checklist when touching a feature that an Agent tool wraps: 1. **API signature drift.** If the service class constructor, method signature, or return type changes, update the tool's `execute()` in `app/.../agent/tool/builtin/`. A stale call compiles only if the old overload still exists; otherwise it's a build break. 2. **`parametersSchema` ↔ `execute()` alignment.** The JSON schema advertised to the LLM must match what `execute()` actually reads from `args`. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it. 3. **`description` accuracy.** The LLM decides *when* and *how* to call a tool solely from `description` + `parametersSchema`. If the feature's behavior changed, update the description text — a misleading description causes silent misuse. 4. **`isReadOnly()` correctness.** `true` → runs without user confirmation; `false` → triggers `PermissionPrompter` dialog. If a tool gains write side-effects, flip it to `false`. 5. **Registration.** New tools must be added to `ToolRegistryFactory.baseTools()` (grouped by domain comment). Forgotten registration = tool invisible to the LLM. 6. **Removed features.** If a feature is deleted, remove or disable its tool. A tool that calls a dead class crashes at runtime inside the agent loop. 7. **Host-only.** Agent tools live under `core/agent/` and are never shell-synced. Do not add them to `syncShellRuntimeSources`. Key paths: | Concern | Path | |---------|------| | Tool interface | `app/.../agent/tool/Tool.kt` | | Tool registry | `app/.../agent/tool/ToolRegistryFactory.kt` | | Tool context (services access) | `app/.../agent/tool/ToolContext.kt` | | Built-in tools (by domain) | `app/.../agent/tool/builtin/*.kt` | | Agent loop / execution | `app/.../agent/engine/AgentEngine.kt` | | Permission prompt (Channel-based) | `app/.../agent/permission/PermissionPrompter.kt` | | LLM provider (SSE streaming) | `app/.../agent/llm/OpenAiCompatProvider.kt` | ### 12. Change editor config-card UI (Compose) The editor screens are built from `DkcSettingCard`s that share one visual grammar. The activation-card series (issue #567: PRs #568 → #571 → #573 → #574) took four attempts because each round invented layout instead of copying the neighbours, and each round was only compile-checked. A short prompt like "改一下 XX 卡的 UI" still means: follow these rules. **Copy, don't invent.** Before writing any card, open the reference implementation on the same screen and mirror it element by element: | You are writing… | Copy from (`CreateAppWebViewCards.kt`) | |------------------|----------------------------------------| | Toggle-headed feature card | `FullscreenModeCard` | | Collapsible card with `DkcChoiceRow` header | `BrowserAdvancedConfigCard` | | Radio group (single-choice) | PWA offline-strategy selector in `sectionOfflinePerformance` | | Sub-toggles / fields inside an expanded zone | `staticAssetPack` block, proxy block | | Nested conditional content | proxy MANUAL/PAC `AnimatedVisibility` swap | Inputs and dialogs follow the same grammar: text inputs are `PremiumTextField`, list-manager dialogs are `Dialog` + `DkcCard` + embedded `TopAppBar` (see `AdBlockSubscriptionSelectorDialog`), and `ActivationCodeCard`'s offline-policy rows are another canonical radio reference. Hard rules learned the hard way: 1. **Two sanctioned card shapes.** Toggle header: `DkcSettingCard { DkcToggleRow(header); AnimatedVisibility(enabled, CardExpandTransition/CardCollapseTransition) { Column { DkcSectionDivider(); full-bleed rows separated by more dividers } } }`. Collapse header: `DkcSettingCard { Column { DkcChoiceRow(header); AnimatedVisibility(expanded) { Column(padding(horizontal = DkcSpacing.RowHorizontal, vertical = DkcSpacing.ContentGap), spacedBy(DkcSpacing.ContentGap)) { … } } } }`. 2. **Alignment grid.** `DkcToggleRow` / `DkcChoiceRow` are full-bleed rows carrying their own 16dp padding. Anything that is *not* a row — text fields, radios, group labels, buttons, notes — must sit inside a `Column(padding(horizontal = DkcSpacing.RowHorizontal, vertical = DkcSpacing.ContentGap))` zone. Sub-toggles inside a zone double-indent (16dp zone + 16dp row); that matches the reference, do not "fix" it. Group labels are `Text(style = labelMedium, color = primary)`, not `DkcSection`. 3. **Radio groups** are exactly: `Text(labelMedium, primary)` label, then `Row(fillMaxWidth().clip(MaterialTheme.shapes.small).clickable { … }.padding(vertical = 4.dp)) { RadioButton; Spacer(4.dp); Text(bodySmall) }`. Never wrap a radio group in `DkcSection`, never attach per-option prose. 4. **Card headers carry title + icon only.** No subtitle on the header row, no free-floating description text next to it. Subtitles belong on child rows that genuinely need one (e.g. 显示状态栏). 5. **Expansion state ≠ feature state.** Never bind a section's `isExpanded` / `AnimatedVisibility(visible=…)` to the feature's `enabled` flag — expanding a panel must never switch the feature on. Section collapse state is its own `remember { mutableStateOf }`. 6. **Conditional sub-blocks** (mode swaps, dependent fields) use `AnimatedVisibility` with `CardExpandTransition` / `CardCollapseTransition`, never bare `if` inside the card body. 7. **Compile ≠ verified.** After any card UI change, build + install on the emulator and check the rendered card: `uiautomator dump` element bounds, compare left edges / row heights of the changed card against its neighbours on the same screen (they must share the same content columns), plus a screenshot pass. Content a few dp off the grid is invisible in code review and obvious on screen. 8. **If a UI rework PR gets "this doesn't match the other cards" feedback**, the fix is realignment to these patterns, not further invention. --- ## Easy-to-miss points - **Keyboard avoidance below API 30 requires the classic window path.** Android 10 and lower have no native IME-inset dispatch: an edge-to-edge window (`decorFitsSystemWindows = false`) is never resized for the keyboard and reports zero IME insets, so no `softInputMode` value helps there. `WindowHelper.applyImmersiveFullscreen` therefore keeps the decor fitting system windows on the RESIZE keyboard path below API 30 (system `SOFT_INPUT_ADJUST_RESIZE` works) and degrades TRANSPARENT/IMAGE status-bar styles to solid colors on that path. Do not re-enable edge-to-edge unconditionally for those devices (#613; #634 flipped only the softInputMode bits and fixed nothing — its Robolectric test passed because it never asserted the layout flags). - **Shared sources are authored in `app/`.** Editing only a file under `shell/src` is usually wrong; it will be overwritten on sync or diverge from host. - **Config field names drift.** Editor model, `ApkConfig`, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. Run `checkConfigFieldDrift`. - **Low targetSdk (28) and fork/exec runtimes** constrain "modernize the shell SDK" changes. - **Incremental export cache** keys must be content-based; mtime and resigned APKs create false hits/misses. - **HTML/FRONTEND file access.** Packaged local-file shells must have `allowFileAccess = true` (forced in `buildWebViewBlock` and `ShellWebViewConfig`); do not regress pure file-based HTML loads. - **Node native libs.** Exported NODEJS_APP needs `libnode_bridge.so` + `libnode.so` + `libc++_shared.so`; missing any causes `loadNode` / `loadJniBridge` failure at runtime. - **16KB page alignment.** `libnode.so` and other large ELF natives must be 16KB-aligned (`ElfAligner16k`) for Android 15+ devices; `node_bridge.cpp` / `node_launcher.c` enable 16KB app-compat before `dlopen`. - **Node JNI output bridge.** `NodeJniOutputBridge` is a stable class referenced by native code; keep its `-keep` proguard rule so R8 does not rename `onOutput`. - **Crashing FGS when notification channel creation fails.** Always use `SafeNotificationChannels` for channel creation. - **Adblock is wired for preview + export.** Do not wipe host filter state during export; the host AdBlocker serves preview and the compiled rule set ships in the APK. - **Runtime permissions are feature-driven.** `RuntimePermissionSync` derives the permission list from enabled features; do not revert to a static template. - **Splash preview media path.** Preview reads splash media from the host filesystem (`splashMediaPath`); export packages it into assets. Do not hardcode `assets/splash_media.*` as the only source. - **Host player UI ≠ shell player UI.** Preview players (`ui/gallery/`, `ui/media/`) and generated-APK players (`ui/shell/`) are separate composables sharing only the config. Port every visible behavior across (thumbnail bar, background, keep-screen-on, orientation) and let `ShellUiParityTest` verify the flags; config flowing end-to-end is necessary but not sufficient. - **Port conflict policy.** Local server runtimes must allocate through `PortManager` and clean up on stop; do not bind ports directly. - **Agent tool ↔ service drift.** When a service class API changes, the corresponding Agent tool in `core/agent/tool/builtin/` must be updated in the same PR. A stale tool either fails to compile or silently passes wrong arguments at runtime. Check `ToolRegistryFactory.baseTools()` for the full tool list. - **Editor card UI grammar.** Config-screen cards share one layout grammar (recipe 12): rows are full-bleed, non-row content sits in 16dp-padded zones, expansion never toggles the feature, and card UI is verified on the emulator — not just compiled. --- ## Forbidden / high-risk mistakes - Second shell template APK - Excluding a shell-synced class but leaving imports/constructors in shell-synced sources - Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps - Putting host-only tools back into shell sync for convenience - OEM push SDKs or unjustified heavy dependencies in shell - Feeding signed/renamed APKs into template/modify paths - Crashing FGS when notification channel creation fails - Committing secrets, keystores, or local machine config - Regressing HTML/FRONTEND file access in packaged shells - Shipping a host player-screen feature without its shell counterpart (preview-only UI) - Shipping NODEJS_APP without `libnode_bridge.so` / `libnode.so` / `libc++_shared.so` - Skipping 16KB alignment for large ELF natives - Inventing editor card layout/spacing/animations instead of copying the neighbouring cards' patterns, or shipping card UI verified only by compilation --- ## Verify commands ```bash ./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache ./gradlew :app:compileStandardDebugKotlin -x syncCloneHostDex --no-configuration-cache ./gradlew :app:checkConfigFieldDrift --no-configuration-cache python3 scripts/check_config_field_drift.py ``` Use these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on `:app` is usually enough. CI note: the PR `check` job compiles only `:shell:compileDebugKotlin` (debug variant) and skips template sync (`-PskipShellTemplateSync=true`). The shell **release** variant (R8 / proguard) and the template pipeline are exercised only by the manual `workflow_dispatch` packaging job — after touching `shell/proguard-rules.pro` or shell packaging, run the first command locally before pushing. Related focused tests often worth running after nearby edits: `ApkBuildCacheTest`, `AdBlockerHostRuntimeTest`, `AdBlockExportWiringTest`, `PortManagerTest`, `BuildInputPreflightTest`, `GoBuildEnvironmentTest`, `RuntimePermissionSyncTest`. --- ## Implementation snapshot Landed: - Single shell template (`webview_shell.apk`) from `:shell` release, full runtime synced from `app/` - Incremental `ApkBuildCache` (`FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`); encrypted builds always full - Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions - `SafeNotificationChannels` fail-soft path for FGS - `PortManager` conflict policies + real stop handlers across Node/PHP/Python/Go/WordPress - `LocalDnsBridgeProxy` wiring for local server runtimes (including Node.js) - Runtime downloads via `NetworkModule.downloadClient` - Adblock preview + export wiring restored - HTML/FRONTEND file-access for packaged local shells - Node.js export: `libnode_bridge.so` + 16KB-aligned `libnode.so` + `libc++_shared.so`; 16KB app-compat before dlopen; stable `NodeJniOutputBridge` JNI callback - Go export: `libgo_exec_loader.so` embedded; in-app build ENOSPC handling + GOTMPDIR relocation - Go host-side builds on W^X hosts: `GoDirectBuilder` replays `compile`/`asm`/`link` as single-shot processes through `HostProcessLauncher` (user-mode static exec loader), with a content-keyed persistent package archive cache (#796) - Multi-web: gallery/media site sources embedded into the APK and resolved at runtime; nested site sources degrade to a URL instead of aborting the build (#798, #792) - Gallery playback: shuffleOnLoop, rememberPosition, overview grid, thumbnail bar, pinch-to-zoom viewer, with host/shell parity enforced by `ShellUiParityTest` (#781, #786, #801) - Untrusted bitmap decodes bounded (oversized-image crash fix) (#788) - Backup/restore coverage aligned with the current storage layout; restarts only when local files change, with throttled progress callbacks (#790, #794) - NativeBridge enabled by default for newly created apps (#805) - Runtime permission sync (feature-driven) - Splash preview media path fallback - Config field drift detection (`checkConfigFieldDrift`) - Module Market: Chrome Web Store live search + GreasyFork browse - Code editor find-and-replace - Security hardening sweep: TLS-fingerprint bridge validates upstream certs (system + custom CAs + hostname) and its local CA is signature-verified with no error-type fallback; JS bridges are caller/origin/scheme-gated (NativeBridge CORS bypass, GM bridge, MV3 `ChromeHostPermissions`); MITM proxy host-allowlisted and CA key wrapped at rest; zip extraction routed through `util/SafeZip`; concurrent exports serialized per package with per-package work dirs; multi-web server-runtime site sources degrade to URL; PHP/Python/WP embed failures fail the build; error pages and `TranslateBridge` callbacks JSON-escaped; keystore password sidecars excluded from backups - Agent tool system: up to 57 tools — 52 base + 2 plan-mode + 3 imagery (image-capable models only) — covering app lifecycle, config templates, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files, and imagery, with Channel-based permission prompting, per-section SSE parse resilience, and plan mode; runtime/build-env tools surface `localExecAllowed` so the LLM knows targetSdk>=29 hosts cannot exec app-storage binaries