agentleFS
shiahonb777shiahonb777/web-to-app/AGENTS.md

web-to-app

AGENTS.md7.2k starsChanged 23 days agoUnlicenseView on GitHub

Instructions for coding agents working in this repository. 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). Mental model:

We split this AGENTS.md into the rules it gives an agent

42 rules. About 0 are common, 19 are distinctive, and 23 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈19

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 23

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Distinctive:

    Do what you believe is right.

    line 7
  2. Distinctive:

    Do not add copyright or license headers unless asked.

    line 10
  3. Very specific:

    Prefer adding properties on the existing split objects (StringsA … StringsE, one object per file).

    line 57
  4. Very specific:

    Avoid new third-party dependencies unless strongly justified (app/build.gradle.kts / shell/build.gradle.kts).

    line 64
  5. Very specific:

    Do not feed signed or renamed APKs back into full modifyApk as templates.

    line 79
  6. Very specific:

    Do not commit secrets, local.properties, keystores, or IDE/cache junk.

    line 92
  7. Distinctive:

    Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).

    line 93
  8. Distinctive:

    When changing export or shell packaging, rebuild the template you touched.

    line 94
  9. Very specific:

    When changing config fields, run checkConfigFieldDrift to catch model ↔ shell config name drift.

    line 95
  10. Very specific:

    Default target: shiaho777/web-to-app.

    line 99
  11. Distinctive:

    Links the user can open (required): When you point at an Issue or PR — in chat, in a commit message, or in an Issue/PR body — give the full …

    line 103
  12. Very specific:

    Do not tell the user to find a finished delivery with is:issue state:open.

    line 110
  13. Distinctive:

    When the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end.

    line 112
  14. Very specific:

    If check fails, fix it on the same branch and push.

    line 115
  15. Distinctive:

    Do not tell the user the delivery is done while the PR is still open, CI is still running or red, or the Issue is still open.

    line 117
  16. Distinctive:

    Do not close the Issue yourself before the PR is merged and CI is green.

    line 119
  17. Very specific:

    Branch naming: use plain type/slug names — fix/…, feat/…, refactor/…, docs/…, perf/…, chore/….

    line 121
  18. Very specific:

    Add the property in the correct Strings* split with all 10 languages.

    line 165
  19. Very specific:

    Coverage test update (REQUIRED): When adding a new Boolean field to WebViewConfig, you MUST add it to flipAllBooleans() in …

    line 177
  20. Distinctive:

    Keep changes surgical; shell ships a thin dependency set and a curated source sync.

    line 204
  21. Very specific:

    If you touch FGS / notification channel creation, fail soft via SafeNotificationChannels.

    line 205
  22. Very specific:

    Keep implementation under host-only packages (core/apkbuilder, host screens, sample/market, core/host, …).

    line 209
  23. Very specific:

    Do not pull host-only deps into shell/build.gradle.kts.

    line 210
  24. Very specific:

    Prefer ApkBuildCache / content hashes over timestamps.

    line 214
  25. Distinctive:

    Do not use signed outputs as templates.

    line 216
  26. Distinctive:

    If template bytes change, ensure cache keys invalidate correctly.

    line 217
  27. Distinctive:

    Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).

    line 221
  28. Distinctive:

    Do not add OEM push SDKs by default.

    line 222
  29. Very specific:

    Follow modules/README.md catalog layout (registry.json + module folders).

    line 227
  30. Very specific:

    Use NetworkModule.downloadClient for large dependency / engine / runtime downloads.

    line 244
  31. Very specific:

    API signature drift: If the service class constructor, method signature, or return type changes, update the tool's execute() in …

    line 269
  32. Distinctive:

    Removed features: If a feature is deleted, remove or disable its tool.

    line 274
  33. Distinctive:

    Copy, don't invent: Before writing any card, open the reference implementation on the same screen and mirror it element by element:

    line 293
  34. Distinctive:

    Card headers carry title + icon only: No subtitle on the header row, no free-floating description text next to it.

    line 310
  35. Very specific:

    Expansion state ≠ feature state: Never bind a section's isExpanded / AnimatedVisibility(visible=…) to the feature's enabled flag — …

    line 311
  36. Distinctive:

    Description text follows the About switch: After the card change, apply the host-UI description rule below to every new subtitle or hint on …

    line 315
  37. Distinctive:

    After every host UI change — editor cards, dialogs, About, home, not only config cards — look at the new prose and decide:

    line 321
  38. Very specific:

    Gate it: when it is secondary explanation: a row subtitle, a "what this does" paragraph, a hint under a title.

    line 323
  39. Very specific:

    Crashing FGS when notification channel creation fails: Always use SafeNotificationChannels for channel creation.

    line 340
  40. Distinctive:

    Adblock is wired for preview + export: Do not wipe host filter state during export.

    line 341
  41. Very specific:

    Agent tool ↔ service drift: When a service class API changes, the corresponding Agent tool in core/agent/tool/builtin/ must be updated in …

    line 346
  42. Very specific:

    Use these when you change shell membership, export packaging, or config fields.

    line 382

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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.
  • Fix root causes, not symptoms. Do not paper over a defect with catch-and-swallow, degraded fallbacks, or UI workarounds that leave the underlying behavior broken. Trace the failure to where it originates and repair it there — even when that is harder.
  • Match the patterns and conventions already in the surrounding code.
  • Do not add copyright or license headers unless asked.

Project layout

PathRole
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.
feature-stacks/Optional-dependency modules bundled as graftable packages (dex/ + stack.arsc at package-id 0x80+ + res/ + curated manifest.xml fragment + stack.json). Built by each module's bundle*Stack task, synced to app/src/main/assets/stacks/ by :app:syncStackBundles, injected into generated APKs only when the export config enables the feature (#1115).
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).
sample-bundles/Heavy sample dependency packs (python-*-shared.zip + sha256-pinned manifest.json) fetched on demand by SampleSharedPackManager — deliberately NOT in app/assets (saves ~30MB raw / ~7MB compressed from the host APK). Regenerate via scripts/build_sample_bundles.py.
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

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
  WebToAppApplication → 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 under app/src/main/java/com/webtoapp/core/i18n/ (facade object Strings in Strings.kt, delegating to StringsA … StringsE, one file per split object).
  • 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 (StringsA … StringsE, one object per file); match surrounding style.
  • Shell gets generated string subsets, not the synced files. syncShellRuntimeSources excludes core/i18n/Strings*.kt; generateShellStrings (→ scripts/generate_shell_strings.py) scans the synced runtime sources for Strings.x / StringsX.y references (aliases val S = Strings handled) and emits reduced Strings.kt / StringsA-E.kt under shell/build/generated/shellStrings carrying only referenced members plus the facade infrastructure — editor-only strings never reach the shell template or generated APKs (~0.75 MB compressed saved per APK). Missing references fail the shell compile loudly; nothing fails silently. Author strings in app/ exactly as before — no extra step needed.

Android and packaging constraints

  • Generated apps ship minSdk 23 / targetSdk 35 (the shell template). ApkExportConfig.targetSdk can pin another target (whitelist chips; process-exec types stay 28) and the AAB path forces DEFAULT_PLAY_TARGET_SDK (36). ApkExportConfig.minSdk can raise the floor only (23..effectiveTargetSdk); lowering below 23 is rejected because the packed DEX requires API 23. The shell runtime is already adapted to 35 (POST_NOTIFICATIONS gating, FGS types, exact-alarm fallback, enableEdgeToEdge) — keep those paths working if the value moves again.
  • The host app targets SDK 36 (antivirus reputation) and floors at minSdk 24 — Play rejects a bundle below 24, and both the standard and gplay flavors share that floor, so the sideloaded APK needs Android 7.0 too. The host floor and the generated-app floor (23) are independent: do not let a generated-app minSdk change move the host, or vice versa. If the host floor moves, update the builder requirement in README.md, .github/docs/README_CN.md, and docs/{,zh/}guide/{getting-started,faq}.md in the same change — raising 23 → 24 in #1385 (v81) left all of them stale at "Android 6.0 (API 23)". Host-side exec of app-data binaries is SELinux W^X-blocked, so downloaded toolchains (esbuild, via NativeNodeEngine) must launch through HostProcessLauncher (core/linux) — it picks ProcessBuilder where exec is allowed or the memfd static exec bridge (StaticExecProcess/RuntimeExecPolicy) on W^X hosts, and returns a clear error when neither is available. A raw Runtime.exec/ProcessBuilder on a downloaded binary fails on the host (#795). The exec bridge is host-only — generated APKs exec nothing, and the bridge classes are excluded from the shell sync.
  • 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.
  • Feature stacks (optional deps, #1115): heavy optional SDKs (admob, …) live in feature-stacks/<id> and graft into the generated APK at export: FeatureStacks.enabledFor(config) gates inclusion, FeatureStackGrafter appends classesN.dex + namespaced res/ + merges the manifest fragment (AxmlRebuilder.mergeManifestFragment) + grafts the stack's resources.arsc package (ArscPackageGrafter, package-id ≥ 0x80). Disabled stacks cost zero bytes. Rules for new stacks:
    • The runtime contract (interface + gates) is authored under app/ so it syncs into the shell; the stack implements it and is reached via Class.forName + fail-soft fallback.
    • Contract types must be name-pinned in shell/proguard-rules.pro (-keep class com.webtoapp.core.ads.api.** { *; }) — shell R8 renames com.webtoapp.**, and a renamed interface resolves to the stack's own copy, silently breaking casts.
    • Contract signatures must carry only framework types or contract-owned types. Kotlin function types (() -> Unit → kotlin.Function0) compile to library classes that shell R8 renames — the interface method then desugars to a renamed parameter type and the stack impl hits AbstractMethodError. Use a fun interface inside the kept api package instead.
    • Stacks must be self-contained: stackEmbedGroups must cover every non-framework dependency the stack's code touches (gms, ump, org.jetbrains.kotlin). The template's kotlin-stdlib is an R8-processed subset with renamed classes AND inlined helpers — a stack referencing kotlin/jvm/internal/Intrinsics gets NCDFE/NoSuchMethodError at runtime. Do NOT "fix" it with -keepnames in shell (names-only doesn't restore inlined methods, and a kept-name partial copy shadows the stack's complete one).
    • mergeManifestFragment writes attributes sorted by attribute resource id — PackageParser's TypedArray lookup requires ascending ids and rejects install (INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: <activity> does not specify android:name) even though aapt2 parses the disorder fine.
    • Stack ids/registry live in FeatureStacks; bundle sha256 feeds the incremental manifestFingerprint.
  • 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.
  • C++ STL asymmetry (deliberate): shell builds c++_static so the template drops libc++_shared.so entirely (all shell natives self-contained); the host keeps c++_shared because injectNodeJsNativeLibs copies libc++_shared.so from the host nativeLibraryDir into NODEJS_APP exports. Do not "unify" the two without rerouting that injection source.
  • Shell keeps com.google.android.material: removing the material widgets library from the shell template regressed generated-app launcher icons on Android 16 (rounded source icons rendered as an inner tile over a solid frame; bisected to 7c8bdc2f, E-good/F-bad/G-good). The shared app/src/main/res/values/themes.xml references Theme.Material3.* parents and M3 color attrs, which must resolve from the real library at template build time. Do not remove the dep or reintroduce a local compat shim without device-testing generated icons on Android 16.
  • Shell release is obfuscated (shell/proguard-rules.pro): com.webtoapp.** members are kept from shrinking but renamed; field names are pinned (<fields> keepclassmembers) so un-annotated Gson model fields keep working. JNI-callback classes and manifest components keep explicit name-preserving rules. Template carries no v1 signature (re-signed at export anyway); apksigner verify on the raw template fails on that basis alone — verify with --min-sdk-version 24.
  • 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/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; 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.

Links the user can open (required): When you point at an Issue or PR — in chat, in a commit message, or in an Issue/PR body — give the full URL and nothing else glued to it:

  • https://github.com/shiaho777/web-to-app/issues/N
  • https://github.com/shiaho777/web-to-app/pull/N

Do not write 1367(已关闭), 1367 (closed), #1367 closed, or author:1367. A number plus a status word is not a link. Pasted into the GitHub Issues search box it becomes a qualifier such as author:1367 (已关闭). author: must be a username, so GitHub rejects the filter (Invalid value … for author) and the issues list fails to load. Put status in its own sentence: the link, then "Closed." or "Merged."

Do not tell the user to find a finished delivery with is:issue state:open. That filter hides closed Issues and merged PRs. To look one up, use the URL above, or a search of the number alone (1376) with no author: and no state:open.

When the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Opening the PR is not the end of the task. Stay with it until the merge is done:

  1. Watch the required check job in "Android CI Build" until it finishes. package-apks skips on PRs; do not wait on it.
  2. If check fails, fix it on the same branch and push. Do not open a second PR for the fix.
  3. Squash-merge only after check is green. Then confirm the Issue is closed by the merge and the head branch is deleted.
  4. Do not tell the user the delivery is done while the PR is still open, CI is still running or red, or the Issue is still open.

Do not close the Issue yourself before the PR is merged and CI is green.

Branch naming: use plain type/slug names — fix/…, feat/…, refactor/…, docs/…, perf/…, chore/…. Do not use tool/agent namespaces (codex/…, devin/…, etc.); the branch belongs to the repo, not the agent.


Shell template and runtime sync

Dual runtime (preview vs export)

Host :appGenerated APK
DEXAll app/src classesShell sync include−exclude (full runtime set)
ConfigEditor / in-memory modelsAssets JSON via ShellModeManager
TemplateNot usedapp/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

ConcernPath
What enters shellshell/build.gradle.kts → syncShellRuntimeSources include/exclude
Shell string subset:shell:generateShellStrings → scripts/generate_shell_strings.py
Shell template build:shell:assembleRelease + :app:syncShellTemplateApk
Template outputapp/src/main/assets/template/webview_shell.apk
Config → shell JSONapp/.../apkbuilder/ApkConfigJsonFactory.kt
Shell config typesapp/.../core/shell/ShellModeManager.kt
Runtime WebView configapp/.../ui/shell/ShellWebViewConfig.kt
Config drift gatescripts/check_config_field_drift.py → :app:checkConfigFieldDrift
Shell minify policyshell/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 ships a thin dependency set and a curated source sync.
  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 60 tools — 55 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:

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:

ConcernPath
Tool interfaceapp/.../agent/tool/Tool.kt
Tool registryapp/.../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 / executionapp/.../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 WtaSettingCards 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 cardFullscreenModeCard
Collapsible card with WtaChoiceRow headerBrowserAdvancedConfigCard
Radio group (single-choice)PWA offline-strategy selector in sectionOfflinePerformance
Sub-toggles / fields inside an expanded zonestaticAssetPack block, proxy block
Nested conditional contentproxy MANUAL/PAC AnimatedVisibility swap

Inputs and dialogs follow the same grammar: text inputs are PremiumTextField, list-manager dialogs are Dialog + WtaCard + 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: WtaSettingCard { WtaToggleRow(header); AnimatedVisibility(enabled, CardExpandTransition/CardCollapseTransition) { Column { WtaSectionDivider(); full-bleed rows separated by more dividers } } }. Collapse header: WtaSettingCard { Column { WtaChoiceRow(header); AnimatedVisibility(expanded) { Column(padding(horizontal = WtaSpacing.RowHorizontal, vertical = WtaSpacing.ContentGap), spacedBy(WtaSpacing.ContentGap)) { … } } } }.
  2. Alignment grid. WtaToggleRow / WtaChoiceRow 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 = WtaSpacing.RowHorizontal, vertical = WtaSpacing.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 WtaSection.
  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 WtaSection, 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.
  9. Description text follows the About switch. After the card change, apply the host-UI description rule below to every new subtitle or hint on the card.

Host UI descriptions (About → 显示说明文字)

The About screen switch Strings.showDescriptions stores ThemeManager.showDescriptions and is read in Compose as LocalShowDescriptions. It hides helper copy. The default is on, so a missed gate still looks fine until the user turns descriptions off.

After every host UI change — editor cards, dialogs, About, home, not only config cards — look at the new prose and decide:

  • Gate it when it is secondary explanation: a row subtitle, a "what this does" paragraph, a hint under a title. Row subtitles use subtitle = Strings.xxx.ifDescriptionsShown(). A block of explanatory text uses AnimatedVisibility(visible = LocalShowDescriptions.current) or if (LocalShowDescriptions.current), the same way neighbouring screens already do.
  • Do not gate titles, the control's current value, errors, confirmation buttons, or status that is the only feedback the user has. ifDescriptionsShown exists so important status stays visible.
  • Shell / generated-APK UI does not read this preference. It is a host display setting. Do not thread it into shell-synced screens.

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.
  • Shell targetSdk 35 is now the baseline — behavior differences (runtime perms, FGS types, BAL, exact alarms) are already handled in synced code; keep the audit items green when touching shell startup/notifications.
  • 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.
  • About "显示说明文字". Helper copy on host UI is gated by the About switch (Strings.showDescriptions → ThemeManager.showDescriptions → LocalShowDescriptions). After every host UI change, decide whether new prose is that kind of helper text and gate it if it is. See "Host UI descriptions" under the card-UI recipe.

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

./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
./gradlew --no-configuration-cache -PskipShellTemplateSync=true -Pfdroid=true \
  :app:compileStandardReleaseKotlin :shell:compileReleaseKotlin \
  :app:processStandardReleaseManifest :shell:processReleaseManifest

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 60 tools — 55 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

Download now (ZIP)

Trending

Quiet since April.thin dataabout the whole repository

Dec 2025: 0 mentionsJan 2026: 1 mentionFeb 2026: 0 mentionsMar 2026: 3 mentionsApr 2026: 1 mentionMay 2026: 0 mentionsJun 2026: 0 mentionsJul 2026: 0 mentionsAug 2026: 0 mentionsSep 2026: 0 mentionsOct 2026: 0 mentionsNov 2026: 0 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the shiahonb777/web-to-app repository that our weekly crawl found: only 5 in all, so this is a hint, not a trend. There is no adoption data for this file yet.
  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.