Compare commits

..
4 Commits
Author SHA1 Message Date
BluscreamandClaude Opus 5.5 5e6abadb95 docs: add software-vision and android-backup-restore skills with android scripts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:00:44 +01:00
BluscreamandClaude Sonnet 5.5 a16a879b1e Point the AFK status file at tsw-plugins/tasks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 04:18:26 +02:00
BluscreamandClaude Opus 5.5 44c2d1502c docs(coding): files 1000 lines max, functions 200 lines max, no earlier split point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:52:48 +02:00
BluscreamandClaude Opus 5 e385e01993 fix(gsc): correct the claim that no off-engine compiler exists
gsc-tool compiles GSC outside the engine and reproduces the engine's own
diagnostics, so the skill's "no compiler, linter, or test harness worth the
name" framing was wrong and sent the reader to load a map instead of running a
50ms check. §1 is now the gate: `--mode comp --game <title> --system pc --dry`,
with the warning that `--mode parse` is syntax-only and accepts code the
compiler rejects, and that `--game` takes the engine codename (h2 for MW2CR)
rather than the marketing name.

A clean compile proves the script loads, not that it works, so the
"behaviour is unverified until the map runs" half is kept and sharpened.

Also adds two §8 entries found the hard way: a local declared inside a block is
not visible after it, and there are no file-scope constants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 03:04:02 +02:00
17 changed files with 1325 additions and 39 deletions

No files matched your search

+3 -1
View File
@@ -9,8 +9,10 @@ A collection of public, reusable agent skills, cheatsheets, and operational work
| Skill File | Scope / Topics Covered |
| :--- | :--- |
| **[`coding-dev.md`](skills-public/coding-dev.md)** | Standing rules for all code work: maximum linter/compiler strictness, never filtering build output, reuse-before-reinvent dependency ordering, file/function size limits, test & regression discipline, verification gates, scope control, commit hygiene, and honest reporting. |
| **[`software-vision.md`](skills-public/software-vision.md)** | Starting from a software idea: turn it into a requirements checklist, search the web first (projects, forks, skeletons, libraries, user reviews), then merge in training-data recall checked against current sources, shallow-clone promising candidates to a temp dir to check them, then present use / extend / fork / start-fresh options before building anything. |
| **[`cli-tooling.md`](skills-public/cli-tooling.md)** | Using command-line tools correctly: reading the help tree instead of guessing flags, preferring an existing CLI over an ad-hoc script, non-interactive invocation, machine-readable output formats, orienting on prior work before starting, script placement & symlinking, and care around destructive commands. |
| **[`afk-sleep-work.md`](skills-public/afk-sleep-work.md)** | Operating unattended and autonomously: continuous forward momentum, avoiding polling traps and deadlocks, host preservation and resource budgeting (disk, packages, processes), isolated tool provisioning, avoiding "last breath" traps on token/quota exhaustion, bypassing or driving GUI launchers, and wake-up checkpoints. |
| **[`android-backup-restore.md`](skills-public/android-backup-restore.md)** | Extracting/backing up/restoring Android data by access tier before a wipe or migration: EDL/firehose & bootloader, custom recovery (TWRP/OrangeFox/CWM), root, device-owner, Shizuku, plain ADB, and on-device apps; FBE decryption limits, per-partition dumps (userdata, EFS/modem/IMEI, persist), SELinux/UID-correct restores, the death of `adb backup`, and app-specific data (WhatsApp, Signal, TOTP/Keystore) that does not survive. |
### 🔤 Per-Language Rules ([`code-languages/`](skills-public/code-languages/))
@@ -25,7 +27,7 @@ A collection of public, reusable agent skills, cheatsheets, and operational work
| **C# / .NET** | [`csharp-dotnet.md`](skills-public/code-languages/csharp-dotnet.md) | `TreatWarningsAsErrors`, nullable enabled, no `.Result`, trimming/single-file publish caveats, SDK shims |
| **C / C++** | [`c-cpp.md`](skills-public/code-languages/c-cpp.md) | full warning set + `-Werror`, sanitizers, RAII/Rule of Zero, target-based CMake, FFI ownership |
| **Lua** | [`lua.md`](skills-public/code-languages/lua.md) | pin the dialect first, `luac -p` + luacheck as the only gate, locals over globals, `pcall` at the boundary, `#t`/pattern traps, embedded & HavokScript hosts |
| **GSC** | [`gsc.md`](skills-public/code-languages/gsc.md) | no compiler or test harness, `isdefined` on everything external, `endon` before the first wait, per-frame budget, precache at load, level-state side effects |
| **GSC** | [`gsc.md`](skills-public/code-languages/gsc.md) | compile off-engine with `gsc-tool --mode comp`, `isdefined` on everything external, `endon` before the first wait, per-frame budget, precache at load, level-state side effects |
Each language file extends `coding-dev.md` rather than repeating it. A project's own
`AGENTS.md` / `CLAUDE.md` / `.rules` always overrides both.
+1 -1
View File
@@ -120,7 +120,7 @@ When remaining budget or context drops below a safe margin:
## 7. Checkpointing & The "Wake-Up" Audit Trail
Maintain a persistent status file (`PROGRESS.md` or session artifact) updated at each milestone:
Maintain a persistent status file (for the tsw-plugins project `tsw-plugins/tasks/{todo,in-progress,done}.md`; otherwise a session artifact) updated at each milestone:
| Section | Contents |
| :--- | :--- |
+562
View File
@@ -0,0 +1,562 @@
---
name: android-backup-restore
description: >-
Use this skill when extracting, backing up, migrating, or restoring data from an Android
device — especially before a wipe, ROM flash, factory reset, or device-to-device move, and
when deciding what is salvageable at a given access level. Covers the full ladder of access
tiers (EDL/firehose & bootloader, custom recovery CWM/TWRP/OrangeFox, root, device-owner,
Shizuku, plain ADB, and on-device apps), file-based-encryption decryption limits, per-partition
dumps (userdata, EFS/modem/IMEI, persist), SELinux/UID-correct restores, the death of
`adb backup`, and app-specific data (WhatsApp, Signal, TOTP/Keystore) that does not survive.
---
# Android Backup & Restore — Extraction by Access Level
How to pull as much as possible off an Android device before wiping, flashing, or migrating —
and how to put it back. Organised as a **ladder of access tiers**: the more privileged the
access, the more you can extract. Work out which tier you have, pull from the highest tier
available, and fall back down the ladder for whatever that tier cannot reach.
This is a *data-recovery and migration* skill for devices you own or are authorised to service.
Shell orchestration here follows [`bash.md`](skills-public/code-languages/bash.md) (strict mode,
hard `timeout`s, no wait-loops); CLI invocation follows
[`cli-tooling.md`](skills-public/cli-tooling.md) (read `<tool> --help` before trusting any flag
below — tool surfaces drift, and the exact partition/block paths are **per-device**).
> [!IMPORTANT]
> **The single biggest variable is encryption.** Every modern Android device uses File-Based
> Encryption (FBE); the `/data` partition is a ciphertext blob until something that holds the
> user credential derives the keys. That "something" is normally the booted OS. A custom
> recovery can only read plaintext `/data` if it was *built to decrypt your exact Android
> version* and you type the lockscreen PIN/password into it. See §3. This is why "boot it
> normally and pull over ADB/root" beats "boot to recovery and dd" whenever the device still
> boots.
---
## 1. The Access Ladder (decision table)
Pick the highest row you can reach. Each tier can extract everything below it plus its own column.
| Tier | Access you need | Reaches raw partitions? | Reaches decrypted `/data/data` app data? | Reaches EFS/IMEI/persist? | Notes |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **EDL / firehose / BROM** (§8) | Vendor edl/mtk loader, device in 9008/BROM | ✅ full flash, offline | ⚠️ only as ciphertext (still FBE-locked) | ✅ | Last resort for dead/bricked devices; dumps are encrypted blobs unless you also have the keys. |
| **Bootloader / fastboot** (§8) | Unlocked bootloader | ⚠️ some (`fastboot fetch` on newer; mostly flash-only) | ❌ | partial | Mostly a *flashing* interface, not a dumping one. |
| **Custom recovery** TWRP/OrangeFox/CWM (§3) | Unlocked bootloader + matching recovery | ✅ every partition via `dd`/built-in backup | ⚠️ **only if recovery decrypts your FBE** + you enter the PIN | ✅ | Classic "nandroid". Decryption is the catch on Android 10+. |
| **Root (booted OS)** (§4) | Magisk/KernelSU/APatch on a booting device | ✅ (block devices readable) | ✅ **fully decrypted** (OS is running) | ✅ | The best tier for a *working* device — plaintext data + raw partitions together. |
| **Device Owner / DPC** (§5) | `dpm set-device-owner` on a fresh/unprovisioned device | ❌ | ❌ (own managed scope only) | ❌ | Niche; grants/denies permissions, reads some providers; not a bulk dumper. |
| **Shizuku / ADB-shell privilege** (§6) | Shizuku (wireless-debug or one-time ADB) | ❌ | ⚠️ APKs, `Android/data`, `Android/obb`, media — **not** `/data/data` of release apps | ❌ | `adb shell` UID (2000) reach, on-device, no PC tether. |
| **Plain ADB** (§6) | USB debugging authorised | ❌ | ⚠️ same as Shizuku + `run-as` for *debuggable* apps only | ❌ | `adb backup` is effectively dead (§7). Pull storage, providers, bugreport. |
| **On-device app, no root** (§6.4) | Just the unlocked phone | ❌ | ❌ (own data only) | ❌ | Vendor cloud/D2D, Seedvault, per-app export. Last-mile for the non-technical case. |
**Rule of thumb for a pre-wipe sweep:** if it boots and you can unlock it → get **root** (§4) if
possible, else **ADB** (§6). If it does *not* boot → **custom recovery** (§3), else **EDL** (§8).
Always grab **EFS/IMEI/persist** (§4.3) before any flash regardless of tier.
---
## 2. First: inventory what is actually worth saving
Before dumping 128 GB of userdata, enumerate. Most of the value is small and specific.
- **Media & documents** — `/sdcard` (a.k.a. `/storage/emulated/0`): DCIM, Pictures, Download,
Documents, Music, ringtones. Reachable at *every* tier including plain ADB. Pull this first; it
is the easy win and the bulk of what users actually miss.
- **App data** — `/data/data/<pkg>` and `/data/user/0/<pkg>` (credential-encrypted, "CE") and
`/data/user_de/0/<pkg>` (device-encrypted, "DE"). Needs root or recovery-decrypt. This is where
game saves, app logins, notes, 2FA seeds live.
- **Scoped external app data** — `Android/data/<pkg>` and `Android/obb/<pkg>` on `/sdcard`.
Blocked from ordinary file managers since Android 11/13 but reachable via ADB/Shizuku/root.
- **Telephony/PIM** — SMS/MMS, call log, contacts, calendar. See §6.2 (providers) and §9.
- **Identity partitions** — EFS/modem (`modemst1`, `modemst2`, `fsg`), `persist`, `efs`. IMEI,
Wi-Fi/BT MAC, sensor calibration. §4.3. **Irreplaceable — back up before any flash.**
- **The things that will NOT survive** no matter the tier — hardware-backed Keystore keys, and
by extension many 2FA/banking apps. §10. Plan migrations for these *before* wiping.
---
## 3. Custom Recovery (TWRP / OrangeFox / CWM) — the offline nandroid
Use when the device will not boot normally, or you want a block-level image independent of the OS.
Requires an **unlocked bootloader** (itself a wipe on most devices — so unlock *before* you have
data worth keeping, or accept the loss). Boot without flashing when you can:
`fastboot boot twrp.img` leaves the installed recovery untouched.
> [!TIP]
> **Try the *installed* recovery before downloading or temp-booting one.** `adb reboot recovery`,
> then `adb shell id` — many custom ROMs ship a current OrangeFox/TWRP whose `adbd` already runs as
> **root** and, crucially, is new enough to **decrypt FBE on the installed Android version** (so
> `/data/data` shows real package names and readable file contents). This beats `fastboot boot`-ing
> an old recovery image: a recovery built from a much older branch often won't boot on a new ROM at
> all (its kernel panics and the device falls back to the OS — you'll land back in Android as
> `uid=2000(shell)`, the tell-tale that it didn't take). Confirm decryption with
> `ls /data/data` (real names, not hex) and by reading one `*/shared_prefs/*.xml`.
### 3.1 The decryption problem (read this before trusting a backup)
TWRP/OrangeFox can only back up **plaintext** `/data` if its decryption code matches your device's
FBE implementation *and* you enter the lockscreen credential in recovery (Keymaster derives the
keys from it). Reality on Android 10+:
- Use a recovery **built from the branch matching your Android version** (e.g. an `android-12.1`+
build for Android 12+). A recovery built from an old branch shows `/data` as garbage folder names
or fails to mount.
- Devices that **shipped on Android ≤11 and were upgraded** to 12+ often cannot be decrypted by any
TWRP build — the keystore/Keymaster path changed. OrangeFox sometimes succeeds where TWRP fails;
try both. If neither decrypts, you can still image the partition but only as ciphertext (useless
without the keys).
- If decryption fails, **do not factory-reset to "fix" it** — that destroys the keys and the data.
Verify decryption worked before relying on it: in the TWRP terminal, `ls -la /data/data` should
show real package names (`com.whatsapp`, …), not hex blobs.
### 3.2 Built-in backup vs. raw `dd`
- **Built-in backup** (TWRP "Backup" / OrangeFox): tick System, Data, Boot, EFS, etc. Produces a
nandroid set under `/sdcard/TWRP/BACKUPS/`. If `/data` decrypted, the Data archive holds plaintext
app data; if not, it backs up the raw (encrypted) partition. Pull the whole folder off afterwards.
- **Raw partition image** when you want a specific partition or the built-in backup won't cover it.
From the recovery `adb shell` or terminal — **block paths are per-device**, find them first:
```bash
# from recovery shell — enumerate by name, DO NOT guess the numbers
ls -l /dev/block/by-name/ # symlinks: userdata, boot_a, modemst1, efs, persist, …
cat /proc/partitions
# image a named partition to portable storage (not back onto /data)
timeout 1800 dd if=/dev/block/by-name/userdata of=/sdcard/userdata.img bs=8M
```
Pull with `adb pull /sdcard/userdata.img` (or write to an SD card / OTG drive). A raw `userdata`
image is only restorable/mountable if you also have the FBE keys — prefer the decrypted built-in
Data backup for app data, and reserve raw images for small partitions (§4.3).
### 3.3 Restore
Restore the built-in nandroid through the recovery UI, or `dd` a raw image back **only onto the
exact same partition of the same device** (never another device's EFS — §4.3). After a decrypted
Data restore across Android versions, expect some apps to misbehave; a clean reinstall + selective
data restore (§4.2) is often more reliable than a whole-partition nandroid across versions.
---
## 4. Root on a booting device — the best tier for a live phone
If the device boots and you can unlock it, root (Magisk / KernelSU / APatch) is the strongest
position: the OS has already derived the FBE keys, so **all app data is plaintext**, and you still
have raw block-device access. (For installing root on this workstation's own test devices, Magisk
APKs/modules may already be on the NAS — see
[check-nas-before-downloading](check-nas-before-downloading.md) before fetching.)
### 4.1 Bulk app-data pull
```bash
# as root, on device or via `adb shell su -c`
# tar preserves owners/modes/symlinks — plain `cp`/`adb pull` loses them (see §4.4)
su -c 'tar -cpf /sdcard/appdata-whatsapp.tar \
/data/data/com.whatsapp /data/user_de/0/com.whatsapp'
# then from the PC:
adb pull /sdcard/appdata-whatsapp.tar
```
Pull `/data/data/<pkg>` (CE) **and** `/data/user_de/0/<pkg>` (DE) for each app — some apps split
state across both. For a whole-device grab, `tar` `/data/data`, `/data/user/0`, `/data/user_de/0`
and `/data/misc` (wifi configs, etc.), but mind disk budget on the pulling host.
> [!TIP]
> **Stream straight off the device with `adb exec-out` — no on-device staging.** When `/data` has
> little free space (you can't write a multi-GB tar back onto the partition you're reading), pipe
> the archive directly to the host, and `dd` images the same way (`exec-out` is binary-safe; plain
> `adb shell` mangles bytes):
> ```bash
> adb exec-out 'cd /data && tar -cp -f - data user_de system misc vendor adb' > data-full.tar
> adb exec-out 'dd if=/dev/block/by-name/boot status=none' > boot.img
> ```
> Toybox/busybox `tar` can't store SELinux contexts and exits non-zero when it skips sockets
> (`unknown file type '140000'` — harmless). Capture contexts separately as a restore sidecar:
> `for d in /data/data/* /data/user_de/0/*; do stat -c '%a %U %G %C %n' "$d"; done`. The per-app
> SELinux *categories* (`:c123,c456`) encode the app sandbox and are **not** regenerable by
> `restorecon` alone — this sidecar is how you put them back.
>
> **Record restore metadata while you have the device.** A `dumpsys package` capture gives, per
> app: `codePath` (→ system/vendor/product/`/data/app`=user), `pkgFlags` (SYSTEM/UPDATED_SYSTEM_APP),
> `installerPackageName` (Play `com.android.vending` vs sideloaded `null`), versionCode, and granted
> runtime permissions; `cmd appops dump` adds special ops (SYSTEM_ALERT_WINDOW,
> MANAGE_EXTERNAL_STORAGE, …). Parse these into a manifest so you can later restore *only* user apps
> and re-grant their exact permissions.
Prefer dedicated tools over hand-rolled tar when restoring to a *different* device or Android
version, because they record ownership/SELinux/permissions that a naive tar restore drops (§4.4):
- **Swift Backup** (root; Shizuku for a weaker subset) — app + data, permissions, SSAID.
- **Neo Backup** (FOSS, OAndBackupX fork; root incl. KernelSU/APatch) — app + data, schedules.
- **OAndBackupX / Migrate** — FOSS nandroid-style app+data sets.
### 4.2 Raw partitions while rooted
Same `dd` from `/dev/block/by-name/` as §3.2, but from the running OS (`su -c dd …`). Good for
grabbing `boot`/`vendor_boot` to patch, or EFS/persist (§4.3) without rebooting to recovery.
### 4.3 EFS / modem / IMEI / persist — back these up before ANY flash
These hold IMEI, baseband calibration, Wi-Fi/BT MAC, and sensor calibration. Corrupting them bricks
the radio permanently and there is no download that restores *your* unique values. **Partition names
and block numbers are per-device — look them up, never copy numbers from a guide.**
```bash
su -c 'ls -l /dev/block/by-name/' | grep -Ei 'efs|modemst|fsg|fsc|persist|nvram|nvdata|protect'
# Qualcomm: commonly modemst1, modemst2, fsg, fsc, persist
# MediaTek: commonly nvram, nvdata, protect_f, protect_s, persist
for p in modemst1 modemst2 fsg persist; do
su -c "dd if=/dev/block/by-name/$p of=/sdcard/efs-$p.img bs=4M"
done
```
Pull the images **off the device and keep copies on a host/NAS** — a backup that lives only on the
partition you are about to wipe is no backup. Restore the same way (`dd … of=/dev/block/by-name/$p`)
**only to the identical partition on the identical device**. Never flash one phone's EFS onto
another — you would overwrite its unique identity with useless values.
### 4.4 Restoring raw `/data/data` tars correctly (UID + SELinux)
A tar of `/data/data/<pkg>` restored by hand will *not* launch unless you fix two things Android
assigns at install time:
1. **Install the app first** (same package, ideally same version) so Android creates the data dir
with the correct per-app UID and SELinux context.
2. Restore the files, then **fix ownership and labels**:
```bash
# find the app's real UID from the freshly-created dir, don't assume 100xx
su -c 'ls -ld /data/data/com.example' # e.g. u0_a142 u0_a142
su -c 'tar -xpf /sdcard/appdata-example.tar -C /'
su -c 'chown -R u0_a142:u0_a142 /data/data/com.example'
su -c 'restorecon -RFD /data/data/com.example' # argument order matters; -v to see work
```
Then reboot before launching. Skipping the UID chown or `restorecon` is the usual reason a
"successful" restore produces an app that crashes on open. Dedicated backup apps (§4.1) do this
bookkeeping for you — hand-tar only when you must.
### 4.5 Getting root headlessly (no GUI taps) when the device boots
If the bootloader is unlocked and you need root to reach §4 but can't tap through the Magisk app,
patch and flash boot entirely from the shell:
1. **Get the *exact* stock boot.img.** The authoritative copy is the live partition — dump it from
a root recovery (§3) with `dd`/`exec-out`. Don't flash a boot.img of unknown origin: check its
cmdline first (`androidboot.hardware=qcom` = Qualcomm vs MediaTek's `bootopt=…` — a wrong-SoC
image bricks the device).
2. **Patch it** with Magisk's own tooling. The Magisk APK is a zip: its `assets/boot_patch.sh` needs
the arm64 libs renamed (`lib/arm64-v8a/libmagiskboot.so`→`magiskboot`, `libmagiskinit.so`→
`magiskinit`, `libmagisk.so`→`magisk`, `libinit-ld.so`→`init-ld`) plus `stub.apk` and
`util_functions.sh` in the same dir, then `sh boot_patch.sh boot.img` → `new-boot.img`.
- Set the flags **explicitly** (standalone `boot_patch.sh` treats unset as false, which can
bootloop): for a modern FBE device with a vbmeta partition and dynamic partitions use
`KEEPVERITY=true KEEPFORCEENCRYPT=true PATCHVBMETAFLAG=false RECOVERYMODE=false LEGACYSAR=false`.
`Failed to patch` on the kernel DTB step is benign (nothing to patch).
- The recovery installer (`META-INF/com/google/android/update-binary`, which auto-detects these
flags via `get_flags()`) is cleaner **but** fails outside a real sideload: it tries to mount
`/system`, which on super/dynamic-partition ROMs is `/dev/mapper/system` and won't mount in a
plain `adb shell`. `boot_patch.sh` with explicit flags sidesteps that.
3. **Flash** `new-boot.img` (verify it unpacks: `magiskboot unpack new-boot.img`) with
`fastboot flash boot` or, from a root recovery, `dd … of=/dev/block/by-name/boot`. Keep the stock
image for instant rollback. Reboot: `magiskd` in `ps -A` + `magisk -V` confirms root took and
survives reboot.
4. **Grant su to the ADB shell** (Magisk denies it by default, silently): Magisk app → Superuser
access = "Apps and ADB" + Automatic response = "Grant", or seed a `policies` row
(`uid=2000 policy=2`) in `/data/adb/magisk.db`. Until then `adb shell su -c …` returns
`Permission denied` even though root is installed and working.
---
## 5. Device Owner (DPC) — narrow, not a bulk extractor
A device-owner app (set with `adb shell dpm set-device-owner <pkg>/<receiver>` on a device with
**no accounts added yet**) gets elevated *policy* powers, not filesystem powers. It is relevant to
extraction only in that it can silently grant its own runtime permissions and read some content
providers that would otherwise prompt — useful for a managed enrolment flow, not for cleaning out a
phone before a wipe. Hard limits:
- `setPermissionGrantState()` for **sensor** permissions (location, camera, mic, body sensors,
activity recognition) is restricted/deprecated for *profile* owners on Android 12+ (silently
returns false); device owners retain more but the surface is shrinking each release.
- It cannot read other apps' `/data/data`, cannot image partitions, and cannot reach EFS.
If you find yourself reaching for device-owner to extract data, the real answer is almost always
root (§4) or ADB providers (§6.2) instead.
---
## 6. ADB & Shizuku — no root, device boots
Plain ADB runs as the `shell` user (UID 2000). Shizuku grants that **same** privilege to on-device
apps (started via wireless debugging or a one-time ADB command), so their reach is identical to ADB
— handy when you have no PC.
### 6.1 External storage & scoped app dirs
```bash
adb pull /sdcard/ ./sdcard-backup/ # DCIM, Download, Documents, etc. — works everywhere
# Android/data and Android/obb are blocked from on-device file managers (11/13+) but
# the adb shell UID can still read them:
adb pull /sdcard/Android/data/ ./android-data/
adb pull /sdcard/Android/obb/ ./android-obb/
```
### 6.2 Content providers (telephony/PIM without root)
`adb shell content query` reaches providers the `shell` UID is allowed to read. What actually works
on a stock device is uneven:
```bash
adb shell content query --uri content://contacts/phones # contacts — usually readable
adb shell content query --uri content://com.android.calendar/events
# SMS/MMS and call_log generally FAIL for the shell UID (need READ_SMS / be default app):
adb shell content query --uri content://sms # typically permission denied
```
For SMS/MMS, call logs, and contacts as restorable files, use an on-device FOSS app rather than
fighting providers over ADB:
- **SMS Import / Export** (`com.github.tmo1.sms_ie`, F-Droid) — SMS+MMS+call log+contacts to
JSON/ZIP, no root, and it *restores* too (set it as default SMS app during restore).
- **Open Android Backup** (mrrfv) — PC-driven, rootless, uses a companion app (not `adb backup`):
restores apps-without-data, internal storage, and contacts (vCard); exports SMS (CSV) and call
log (txt) as **view-only** (no restore, no MMS attachments).
- Contacts also export cleanly from the stock Contacts app ("Export to .vcf") or via Google account
sync — often the least-effort path for the non-technical case.
### 6.3 `run-as` — app data for *debuggable* apps only
```bash
adb shell run-as com.example.debug tar -cpf - ./ > example-data.tar
```
`run-as` only works for apps with `android:debuggable="true"` (i.e. your own dev builds, not Play
Store releases). Shizuku-based file managers use the same `run-as` path and inherit the same limit.
Release-app `/data/data` is **not** reachable at this tier — that needs root (§4) or a
recovery-decrypt (§3).
### 6.4 Bugreport & app lists (cheap, always worth grabbing)
```bash
adb bugreport ./bugreport.zip # logs, settings dump, installed-package metadata
adb shell pm list packages -3 -f # third-party packages + APK paths, to reinstall later
adb shell cmd package list packages --show-versioncode
```
To re-pull the APKs themselves for reinstall:
```bash
adb shell pm path com.example # -> package:/data/app/.../base.apk + splits
adb pull /data/app/~~.../com.example-.../base.apk # repeat for each split; reinstall with
adb install-multiple base.apk split_config.*.apk # install-multiple
```
### 6.5 Vendor D2D and cloud (no-tech last mile)
- **Google** cloud backup + Pixel/Android cable "device-to-device" restore at setup. App data rides
along *only* if the Google **account** is transferred (Block Store is account-bound); untick
nothing under "Accounts". `allowBackup="false"` stops cloud backup but D2D still transfers app data
unless the app sets `dataExtractionRules`/`android:fullBackupOnly` to opt out.
- **Samsung Smart Switch** (PC or cable): backs up most app data, but **encrypted app data (e.g.
WhatsApp), banking logins, and sideloaded APKs do not transfer** — handle those per §9/§10.
- **Seedvault** (CalyxOS/GrapheneOS/LineageOS): the FOSS backup transport. On Android 15 builds its
**device-to-device** mode (Expert settings) is default-on and captures far more app data than the
old cloud-style transport, because it presents as a D2D migration to each app.
---
## 7. The death of `adb backup` (don't build a plan on it)
`adb backup` was deprecated in Android 10 and gutted in Android 12 (API 31): app data is excluded by
default and only included if the app is **debuggable**, so on essentially all release apps across
Android 12–16 `adb backup` produces an archive with no app data. Treat it as unavailable:
- Don't rely on it for a migration. Use root tooling (§4.1), on-device FOSS exporters (§6.2), or
vendor/Seedvault D2D (§6.5) instead.
- `adb restore` likewise cannot restore encrypted backups and is a dead end for modern data.
- The `bmgr`/LocalTransport backup-manager plumbing still exists for *developers testing their own
backup agents* (`adb shell bmgr …`), but it is a per-app developer tool, not a device-wide
extractor — out of scope for a pre-wipe sweep.
---
## 8. EDL / firehose / BROM — dead or bootloader-locked devices
The lowest level, for devices that will not boot to Android *or* recovery. These talk to the SoC's
emergency loader below the OS:
- **Qualcomm EDL (9008)** — [`bkerler/edl`](https://github.com/bkerler/edl) with a firehose
programmer (`.mbn`/`.elf`) matching the SoC. `edl printgpt` lists partitions; `edl rl dumps
--skip=userdata --genxml` dumps all partitions (minus the huge userdata) to a folder with a
`rawprogram` XML; `edl r <partition> <out.img>` grabs one. Newer SoCs need `--loader=<file>`
because loader autodetection no longer works.
- **MediaTek BROM** — [`bkerler/mtkclient`](https://github.com/bkerler/mtkclient). `./mtk printgpt`,
`./mtk rl out` dumps all partitions to `out/`, `./mtk r <partition> <out.img>` for one; `--preloader`
for BROM mode.
The hard limit: **these dumps are ciphertext.** EDL reads the raw flash below the OS, so `userdata`
comes out FBE-encrypted and is not decryptable without the keys (which live in secure hardware keyed
by the lockscreen credential). EDL is therefore excellent for **small identity partitions**
(EFS/persist/nvram — §4.3), full-flash forensic images, and un-bricking, but it is **not** a way to
read a locked phone's app data. Treat downloaded dumps/tools as untrusted input: keep each in its own
directory, pass paths as arguments, and run any helper scripts from outside the dump directory.
---
## 9. App-specific data that needs its own handling
Some high-value data lives behind app-level encryption and ignores the tier ladder:
- **WhatsApp** — local backup at `/sdcard/WhatsApp/Databases/msgstore.db.crypt15` (+ `/Media`). If
the user enabled **end-to-end encrypted backups**, decryption needs the **64-digit key** (or the
`encrypted_backup.key` file from `/data/data/com.whatsapp/files/`, root-only). Tools:
[`wa-crypt-tools`](https://pypi.org/project/wa-crypt-tools/). Easiest migration is WhatsApp's own
built-in transfer or Google-Drive backup with the same phone number — pull the `crypt15` + media
as insurance, not as the primary path.
- **Signal** — does **not** sync to any cloud. Migrate with Signal's in-app device-transfer, or its
encrypted local backup (Settings → Chats → Backups) whose **30-digit passphrase you must record** —
there is no recovery without it.
- **Authenticator / 2FA** — Google Authenticator exports via its QR "export accounts" flow;
**Aegis** (FOSS) exports an encrypted JSON vault. Do this *before* wiping; TOTP seeds not exported
are gone.
---
## 10. What will NOT survive a wipe (plan around it)
No tier recovers these; the only mitigation is to migrate them *before* the wipe:
- **Hardware-backed Keystore keys** are non-exportable by design — key material never leaves the
TEE/StrongBox. Any app that pinned a device-bound key (banking apps, some password managers,
FIDO/passkeys stored in hardware) will treat the restored/new device as untrusted and re-enrol.
- **Play Integrity / SafetyNet attestation** fails on rooted or custom-ROM devices independently of
your data — so a perfectly-restored banking app may still refuse to run. That is an attestation
problem, not a backup problem; don't chase it as data loss.
- **SSAID / `ANDROID_ID`** changes on wipe (per app-signing-key + user), which can log apps out or
reset entitlements even when their `/data/data` is restored byte-perfect.
Before wiping: export 2FA (§9), confirm password-manager vaults sync to their cloud, record Signal
(§9) and WhatsApp keys, and re-enrol passkeys/banking on the new device rather than expecting a
restore to carry them.
---
## 11. Pre-wipe checklist (the one-screen version)
1. **Decide the tier** (§1). Booting + unlockable → root (§4) or ADB (§6). Not booting → recovery
(§3) → EDL (§8).
2. **EFS/modem/persist first** (§4.3) — before any unlock or flash, off-device.
3. **Media & docs** — `adb pull /sdcard` (§6.1).
4. **App data** — root tar + dedicated tool (§4.1), or recovery-decrypted Data backup (§3).
5. **Scoped dirs** — `Android/data`, `Android/obb` (§6.1).
6. **PIM** — SMS/call-log/contacts via SMS Import-Export or vCard (§6.2).
7. **App-specific** — WhatsApp/Signal/2FA via their own flows (§9).
8. **App list + APKs** — `pm list packages -3`, pull splits (§6.4) to rebuild the app set.
9. **System state the app tars miss** (§12.1) — keystore DB, SSAIDs, URI grants, doze whitelist,
app-ops, launcher/shortcuts/QS/controls, Wi-Fi, SD-card UUIDs, root grants, old `packages.list`.
10. **Baseline + app-sweep** the old ROM (`measure.sh`, `test-all-apps.sh`, §12.4) so the new one
has something to be compared against.
11. **Passwords apps keep themselves** — App Manager's KeyStore password, backup-app passphrases,
Signal/WhatsApp keys (§9). Write them down *before* the wipe; none are recoverable after.
12. **Verify the backups off-device** (open the archive, check `ls /data/data` really decrypted)
*before* you type `fastboot -w` or confirm the reset. A backup you have not read back is not a
backup.
---
## 12. Restoring onto a new ROM (root) — what a data tar does not carry
Learned from a full LineageOS-based A16 → Infinity-X A17 switch (Redmi Note 7, Magisk). Restoring
`/data/data` + `user_de` + `Android/data` per §4.4 brings back most apps, but **every app gets a new
UID** on the new ROM, and a lot of state is keyed by UID or lives in system files. The scripts in
[`scripts/android/`](skills-public/scripts/android/) automate the parts below; they take the
device from `ANDROID_SERIAL` like `adb` itself.
### 12.1 Grab these too, before the wipe (all root, all small)
| What | Where | Why it matters on restore |
| :--- | :--- | :--- |
| Old UID map | `/data/system/packages.list` | Remap everything keyed by UID (SSAID, keystore, grants) |
| Keystore2 DB | `/data/misc/keystore/persistent.sqlite` | Apps whose keys vanish show "Keystore error"/log out (§12.3) |
| SSAIDs | `/data/system/users/0/settings_ssaid.xml` | `ANDROID_ID` change logs apps out (GitHub, Bolt, Lovense, …) |
| Persistent SAF grants | `/data/system/urigrants.xml` | Folder pickers (music players, camera, sync apps) lose their folders |
| Doze whitelist | `/data/system/deviceidle.xml` | Notification/automation apps silently stop in background |
| App-ops & special access | `cmd appops dump`, `cmd notification` policy access, `dumpsys package` | `MANAGE_EXTERNAL_STORAGE`, usage stats, DND, `READ_LOGS`, install-packages, notification listeners are **not** runtime permissions |
| Launcher + pinned shortcuts | launcher DB, `/data/system_ce/0/shortcut_service/` | Home screen; pinned shortcuts (e.g. Ice Box) are owned by the system, not the launcher |
| QS tiles, device controls | `settings get secure sysui_qs_tiles`; SystemUI `user_de` `files/controls_favorites.xml` + `shared_prefs/controls_prefs.xml` | Home Assistant controls, dev tiles |
| Wi-Fi | `/data/misc/apex/com.android.wifi/` or `cmd wifi list-networks` + PSKs | Also note which SSIDs must **not** auto-join |
| Root grants + Magisk settings | `magisk --sqlite "SELECT * FROM policies"`, `… FROM settings`, `… FROM denylist`, `/data/adb/modules` (note which are disabled) | UIDs change, grants don't follow; Zygisk off ⇒ DenyList silently does nothing |
| Accounts | `dumpsys account`; `/data/system_ce/0/accounts_ce.db` | Shows which logins (2nd Google account, app-registered accounts) must be re-added by hand — they can't be transplanted |
| SD-card volume IDs | `blkid` of each SD partition | Public volume ID = filesystem UUID; reformat ⇒ new ID ⇒ broken paths and grants |
| Freezer state | `pm list packages -d` + the freezer app's own list | Updates/reinstalls unfreeze |
### 12.2 Restore order and traps
- **Update first, restore data second.** Installing the old APKs and then the store's update is
fine; restoring data *into* an older APK than the data was written by is a downgrade — apps wipe
or crash on it. Never re-run a full restore pass over updated apps; restore data only.
- **Installer of record:** `pm set-installer` refuses even root. Reinstall the same APKs with
`adb install-multiple -r -i <installer>` (keeps data) so Play/Aurora/F-Droid/Obtainium see their
apps as updatable (`fix-installers.py`).
- **New signature, same package** (forks such as Shizuku → Shizuku-Next for a newer Android): tar the
data dirs, uninstall, install, untar, chown/chcon, re-grant, re-freeze — `reinstall-keep-data.sh`.
- **System files are read only at `system_server` start.** Write `urigrants.xml`,
`settings_ssaid.xml`, `shortcut_service/*` etc. (owner `system:system`, mode 600,
`u:object_r:system_data_file:s0`), then `kill -9 $(pidof system_server)` — a soft restart that
does **not** save over them first, which a normal reboot does. It locks the user: unlock with
wake → swipe → `input text <PIN>` → ENTER (the bouncer screenshots black; retry if needed).
- **fs-verity:** files under `shortcut_service` are verity-protected — `rm` then write, never edit.
- **Restored caches can kill an app:** Ice Box OOM-crashed on a restored MMKV package cache. Drop
`cache`, `code_cache`, WebView caches and per-app package-list caches; keep the real state.
- **Re-map UID-keyed state** with the old/new `packages.list` (SSAID `name="<uid>"`, keystore
`namespace`, Magisk `policies.uid`, doze lists are by package and need no remap).
- **SD card:** if the partition had to be recreated, set the old UUID back
(`tune2fs -U <old>` for ext4, `fsck` before and after) rather than fixing every app.
- **Wi-Fi:** restored networks auto-join in their own order; re-add the ones that must stay
manual with `cmd wifi add-network <ssid> wpa2 <psk> -d`.
- **QS tiles** on A17: `settings put secure sysui_qs_tiles` is ignored; use
`cmd statusbar set-tiles "<list>"`. Developer tiles must be enabled first
(`pm enable 'com.android.settings/.development.qstile.DevelopmentTiles$SensorsOff'`).
- **Launcher switch** (Nova had no build for the new Android): converting Nova's `favorites`
table into Lawnchair's Launcher3 schema works (`nova-to-lawnchair.py`) — mark widgets
`restored=1` so Launcher3 binds fresh widget IDs, and re-pin deep shortcuts for the new launcher.
**Banking-app root checks** are not a restore problem, but they show up right after one. Order
that worked (2026-10, Magisk 31, A17): first the ROM's own spoofing settings, if it has any (PIF,
TrickyStore target and keybox, hide developer status). If an app still crashes on purpose, look for
an Xposed module that defuses that app's check. For PayPal 10.18 that was oslokrok on Vector, the
LSPosed successor. Configure Vector headlessly with
`/data/adb/modules/zygisk_vector/cli modules enable <pkg>` and `cli scope set <pkg> <app>/0`.
**Take the app off the Magisk DenyList**: the DenyList strips every Zygisk module from the process,
including the hook.
### 12.3 Keystore: transplanting app keys
Hardware-backed keys never leave the TEE, but keystore2 stores them **wrapped** in its SQLite DB.
If the new ROM runs on the same TEE (compare `keyentry.km_uuid` old vs new — same device, same
firmware ⇒ same), the old wrapped blobs still unwrap. Keys whose `blobmetadata` has tag 0
(`EncryptedBy`) are additionally super-encrypted with the **old lock-screen credential's** key and
cannot be moved. Domain 0 (APP) rows have `namespace = uid` → remap. Stop the apps, back up the live
DB, insert `keyentry`/`blobentry`/`blobmetadata`/`keyparameter`/`keymetadata` with fresh ids, reboot.
Apps that already regenerated keys on the new ROM also need their data re-restored to match.
`scripts/android/keystore-transplant.py OLD_DB [--replace] OLD_UID:NEW_UID …` writes the SQL; dry-run
it on a host copy of the live DB (`pragma integrity_check`) before applying with the device's own
`sqlite3`. Tested: transplanted keys load with no keystore errors after a reboot. That does not
bring back sessions the server ended, or apps that refuse to run because their build is too old.
### 12.4 Verify like a user would
- `test-all-apps.sh OUT` cold-starts every user app (unfreezing and re-freezing), screenshots it,
and greps its log for crashes, ANRs and keystore/crypto errors → `results.tsv`. Look at every
screenshot: logouts, licence checks, ToS/"what's new" dialogs and permission prompts don't crash.
- Android 17 shell launches must match the activity's intent filter:
`am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n 'pkg/act'`
(single-quote it — component names contain `$`). `-n` alone gives "Access blocked".
- `measure.sh OUT [pkg…]` snapshots memory, crashes/ANRs/lmk kills since boot and cold-launch
times; run it on the old ROM before the wipe so there is a baseline.
- Keep a running log of every security setting weakened during the restore (adb stay-awake, root
grants, doze exemptions, …) with its revert command; the restore needs many of them and nobody
remembers them afterwards.
+36 -11
View File
@@ -4,7 +4,7 @@ description: >-
Use this skill when writing, reviewing, or debugging GSC (Game Script / Call of Duty script)
or CSC — including entity-vs-level scope, threads and notify/wait semantics, endon/waittill
discipline, undefined-value guards, the no-OOP/no-exceptions reality, asset precaching,
per-frame cost, and the fact that GSC has no compiler, linter, or test harness worth the name.
per-frame cost, block scoping, and compiling off-engine with gsc-tool.
---
# GSC — Rules & Preferences
@@ -13,10 +13,10 @@ Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies
GSC is the Call of Duty engine's own scripting language (`.gsc` server-side, `.csc`
client-side). It is **not Lua, not JavaScript, and not C** despite resembling all three. It has
no type system, no exceptions, no classes, no closures in most dialects, and — in a modded
install — **no diagnostics beyond the in-game console**. That last fact drives most of what
follows: the language will not tell you that you are wrong, so the code has to be written so
that it cannot be.
no type system, no exceptions, no classes, no closures in most dialects, and no test harness.
It can be compiled off-engine (see §1), which catches syntax and scope errors — but nothing
checks a type, a field that was never set, or a builtin that does not exist in the target title.
So the code still has to be written so that it cannot be wrong.
> [!IMPORTANT]
> **Establish the dialect before writing anything.** IW3/IW4 (MW/MW2), IW5 (MW3), T5/T6 (Black
@@ -27,18 +27,38 @@ that it cannot be.
---
## 1. There Is No Gate — Build One
## 1. The Gate — `gsc-tool`, Then The Game
GSC has no `gsc -Wall`. What exists:
**There is an off-engine GSC compiler: `gsc-tool`.** Reach for it first; it is
easy to assume none exists and verify by loading a map instead.
```bash
gsc-tool --mode comp --game <title> --system pc --dry path/to/script.gsc
```
It exits non-zero and prints the engine's own diagnostic, e.g.
`[ERROR]:compiler:script:97:25: local variable 'previous_count' not found`.
- **Use `--mode comp`, not `--mode parse`.** `parse` is syntax-only and happily
accepts code the engine's compiler rejects, including the block-scope error
above. `comp` with `--dry` writes nothing and does the real thing.
- **`--game` must match the title**, and it is not the marketing name: MW2
Campaign Remastered is `h2`, MWR is `h1`. The choices are `iw5 iw6 iw7 iw8
iw9 s1 s2 s4 h1 h2 t6 t7 t8 t9 jup`. Wrong title, wrong builtin table.
- Prebuilt binaries ship on its GitHub releases; there is no distro package.
- Many mod trees already vendor it (Overlord has it under `deps/gsc-tool`).
What that still leaves:
| Check | How |
| :--- | :--- |
| **Syntax** | The engine's own compile at load. Read the console; a parse error names the file and line and then stops the whole script loading. |
| **Syntax + semantics** | `gsc-tool --mode comp`. Put it in the build gate. |
| **Builtin exists for this title** | `gsc-tool`'s own engine tables, e.g. `src/gsc/engine/h2_meth.cpp`. Grep before using a builtin you remember from another title. |
| **Runtime errors** | In-game console / `developer 1`. Many mod loaders print a script stack trace. |
| **Static lint** | Third-party only, and dialect-specific. Use one if it exists for the target title; otherwise there is none. |
| **Tests** | None off-host. Behaviour is verified by playing the map. |
Therefore:
So a compile is cheap and mandatory, and **behaviour is still unverified until
the map runs.** Both halves matter:
- **`developer 1` (or the loader's equivalent) is mandatory during development.** Without it the
engine suppresses script warnings and an undefined-value error looks like "the feature did
@@ -48,7 +68,7 @@ Therefore:
- **Instrument with `iprintln` / `iprintlnbold` / `println`** on the success *and* the failure
path. A function that silently returns on a guard is indistinguishable from one that never ran.
- **Say plainly in your report that GSC behaviour is unverified** unless you actually loaded the
map and watched it. There is no test suite to hide behind.
map and watched it. A clean `gsc-tool` compile proves the script loads, not that it works.
---
@@ -211,6 +231,11 @@ init()
functions over reimplementing them. `maps\_utility` and `common_scripts\utility` already have
flag, wait, and array helpers.
- **`/* */` comments do not nest.** Commenting out a block that contains one breaks the file.
- **A local declared inside a block is not visible after it.** Writing it in one `if` and reading
it in a second does not compile — declare it unconditionally at function scope. Assigning
`undefined` to an entity field unsets it, which is usually the right "restore nothing".
- **No file-scope constants.** `MY_CONST = 0.2;` outside a function is not valid in most
dialects; use a local at the point of use or a `level.` field set during init.
- File and function size limits from the general rules apply, and matter more here than usual:
there is no debugger worth the name, so a 300-line function is unanalysable.
- **Delete dead code.** With no linter to flag it, commented-out GSC accumulates forever.
+3 -3
View File
@@ -139,9 +139,9 @@ Attach an `AbortController` to every long-lived `fetch` and abort it in cleanup.
- **Imports**: prefer the configured alias (`@/…`) over relative climbs.
- Indentation and quote/semicolon style: **match the file you are editing**. Do not introduce
a new formatter or lint config into a project that already has one.
- **One file until it earns a split** — around 250 lines for a self-contained plugin/feature,
or when a second file is genuinely reusable (`utils.ts`, `types.ts`, `components/`).
Hard limits from the general rules still apply: ESLint's `max-lines` and
- **One file until it earns a split** — when it nears the 1000-line limit, or when a second
file is genuinely reusable (`utils.ts`, `types.ts`, `components/`).
The limits from the general rules (1000 lines per file, 200 per function) apply: ESLint's `max-lines` and
`max-lines-per-function` cover both natively, so prefer those over a separate test here.
---
+2 -3
View File
@@ -225,9 +225,8 @@ When talking to a rate-limited API (a chat platform, a store, a tracker):
function-length rule — `PLR0915` (`too-many-statements`, default 50) and `C901`
(cyclomatic complexity) are the closest proxies and are worth enabling, but neither
measures lines. Put **both** the function-length and file-length limits in a pytest
case (see [`coding-dev.md` §4](skills-public/coding-dev.md)): file hard 1000 / soft 600 (soft
prints a note rather than failing), function hard 100 measured by walking the `ast` rather than
counting lines of text. **Guard the guard** with a test asserting the glob matched anything at
case (see [`coding-dev.md` §4](skills-public/coding-dev.md)): file 1000 lines max, function
200 lines max measured by walking the `ast` rather than counting lines of text. **Guard the guard** with a test asserting the glob matched anything at
all — a pattern that matches nothing makes every limit vacuous and still passes.
---
+6 -2
View File
@@ -73,8 +73,12 @@ workspace = true
wrong for the *entire* codebase — not a shortcut for one stubborn call site. It silences
the rule in code nobody has written yet.
- **Re-test every `#[allow]` periodically.** If removing one produces no warning, delete it.
- `too_many_lines` is the mechanical enforcement of the 100-line function limit. A function
that trips it wants extracting, not an allow.
- `too_many_lines` is the mechanical enforcement of the 200-line function limit. Clippy's
default threshold is 100, so set it in `clippy.toml`:
```toml
too-many-lines-threshold = 200
```
A function that trips it wants extracting, not an allow.
- Clippy has **no file-length lint**, so the 1000-line file limit belongs in a test — see
[`coding-dev.md` §4](skills-public/coding-dev.md) for the pattern. Put it in the workspace
root's `tests/` so one test covers every member crate, walking `src/` plus each
+26 -18
View File
@@ -26,6 +26,14 @@ reasons attached, not style trivia — most exist because ignoring them caused a
> When in doubt about which mode you are in, **ask** — it changes the answer on strictness,
> file size, typing, and test policy all at once.
> [!IMPORTANT]
> **A new software idea starts with research, not code.** When the user describes a tool, app,
> service, or other project they want built, follow
> [`software-vision.md`](skills-public/software-vision.md) first: find existing projects to use,
> extend, or fork (web searches and user reviews first, then training-data recall, then
> throwaway clones), and present the options before scaffolding anything — unless the user
> explicitly opted out of research.
Per-language specifics live in [`code-languages/`](skills-public/code-languages/):
| Language | Rules |
@@ -115,7 +123,9 @@ a fresh implementation will rediscover slowly and incompletely.
Before hand-rolling any subsystem — a protocol client, a parser, a date formatter, a
debounce, a retry/backoff, a semver comparator — **search first and state the finding.**
Prefer official/first-party SDKs over third-party wrappers.
Prefer official/first-party SDKs over third-party wrappers. The same ordering applies to a
whole project idea, not just a subsystem — see
[`software-vision.md`](skills-public/software-vision.md).
### The corollary: new generic code becomes its own package
@@ -138,19 +148,23 @@ depends on those.
## 4. Size Limits — Files and Functions
| Unit | Soft target | Split at | Hard limit |
| :--- | :--- | :--- | :--- |
| **Source file** | ~400 lines | 600 lines | **1000 lines** |
| **Function** | under 50 lines | — | **100 lines** |
| **Function parameters** | 3–4 | — | ~5, then pass a struct/object |
| **Nesting depth** | 2 | — | 3, then extract |
| Unit | Limit |
| :--- | :--- |
| **Source file** | **1000 lines** max |
| **Function** | **200 lines** max |
| **Function parameters** | ~5, then pass a struct/object |
| **Nesting depth** | 3, then extract |
These are the limits, not targets to stay well under: there is no earlier "split at" point.
A file of 700 lines with one coherent responsibility is fine and is not split for its size.
### Enforce both mechanically, never by eye
A limit nobody checks is a suggestion. Pick the **highest** applicable option:
1. **A linter rule**, where one exists — the function limit is usually covered natively
(Clippy's `too_many_lines`, ESLint's `max-lines-per-function`, `golangci-lint`'s `funlen`).
(Clippy's `too_many_lines` with `too-many-lines-threshold = 200`, ESLint's
`max-lines-per-function`, `golangci-lint`'s `funlen`), configured to 200 lines.
A function that trips it wants extracting, not a suppression. Note that some linters only
offer *proxies* for length — Ruff's `PLR0915` counts statements, not lines — in which case
enable the proxy and put the real limit in a test.
@@ -160,24 +174,18 @@ A limit nobody checks is a suggestion. Pick the **highest** applicable option:
repository rather than living in one machine's shell script.
3. **The build script**, only as a fallback when the project has no test suite to hang it on.
Whichever you use, it should **fail over the hard limit** and separately *name* anything past
the soft split point — so the seam gets found while splitting is still cheap, rather than at
the moment the hard limit finally trips.
Whichever you use, it should **fail over the limit** and name the offending files.
```rust
#[test]
fn no_source_file_exceeds_limits() {
const HARD: usize = 1000;
const WARN: usize = 600;
const LIMIT: usize = 1000;
let mut over = Vec::new();
for path in source_files() {
let lines = std::fs::read_to_string(&path).unwrap().lines().count();
if lines > HARD {
over.push(format!("{} — {lines} lines (limit {HARD})", path.display()));
} else if lines > WARN {
// Not a failure: surfaces the seam while splitting is still cheap.
eprintln!("note: {} is {lines} lines, approaching the {HARD} limit", path.display());
if lines > LIMIT {
over.push(format!("{} — {lines} lines (limit {LIMIT})", path.display()));
}
}
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Reinstall each restored app with its original installer (pm set-installer refuses root).
Same APKs, `install-multiple -r -i <installer>` keeps data. The device comes from ANDROID_SERIAL, as for adb. Usage: fix-installers.py BACKUP_DIR WORK_DIR
"""
import re
import subprocess
import sys
import time
from pathlib import Path
backup, work = Path(sys.argv[1]), Path(sys.argv[2])
log_file = (work / "restore.log").open("a")
def log(msg: str) -> None:
line = f"{time.strftime('%H:%M:%S')} {msg}"
print(line, flush=True)
log_file.write(line + "\n")
log_file.flush()
def adb(*args: str, timeout: int = 900) -> str:
r = subprocess.run(["adb", *args], capture_output=True, text=True, timeout=timeout)
return (r.stdout + r.stderr).strip()
def installers(text: str) -> dict:
# `pm list packages -i` prints "package:PKG", with -f "package:PATH=PKG".
return dict(re.findall(r"[:=]([\w.]+)\s+installer=(\S+)", text))
want = installers((backup / "rooted-fresh/packages-3.txt").read_text())
now = installers(adb("shell", "pm list packages -3 -i"))
disabled = set(re.findall(r"package:([\w.]+)", adb("shell", "pm list packages -d")))
log("=== installer fix-up start")
for pkg, inst in sorted(want.items()):
if inst == "null" or pkg not in now or now[pkg] == inst or inst not in now | {"com.android.vending": 1}:
continue
apks = sorted(str(p) for p in (backup / "apks" / pkg).glob("*.apk"))
if not apks:
continue
# Reinstalling re-enables a frozen app; disable it again afterwards.
out = adb("install-multiple", "-r", "-d", "-i", inst, "--bypass-low-target-sdk-block", *apks)
if pkg in disabled:
adb("shell", f"su -c 'pm disable-user --user 0 {pkg}'")
log(f"{pkg}: installer {now[pkg]} -> {inst}: {'OK' if 'Success' in out else 'FAILED ' + out[-200:]}")
log("=== installer fix-up done")
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Emit SQL that copies apps' keystore2 keys from an old ROM's DB into the live one, under the new UIDs.
Only works when both ROMs use the same TEE (compare keyentry.km_uuid). Keys super-encrypted with the
old lock-screen credential (blobmetadata tag 0, EncryptedBy) cannot be moved and are skipped.
Usage: keystore-transplant.py OLD_DB [--replace] OLD_UID:NEW_UID [...] > transplant.sql
--replace first delete the app's keys that already exist on the new ROM (needed when it regenerated them)
Apply with the apps force-stopped: `su -c 'sqlite3 /data/misc/keystore/persistent.sqlite < transplant.sql'`, reboot.
"""
import secrets
import sqlite3
import sys
DOMAIN_APP = 0
TAG_ENCRYPTED_BY = 0
def lit(v: object) -> str:
if v is None:
return "NULL"
if isinstance(v, bytes):
return f"X'{v.hex()}'"
if isinstance(v, str):
return "'" + v.replace("'", "''") + "'"
return str(v)
def delete_keys(where: str) -> list[str]:
ids = f"(SELECT id FROM keyentry WHERE {where})"
return [f"DELETE FROM blobmetadata WHERE blobentryid IN (SELECT id FROM blobentry WHERE keyentryid IN {ids});",
f"DELETE FROM blobentry WHERE keyentryid IN {ids};",
f"DELETE FROM keyparameter WHERE keyentryid IN {ids};",
f"DELETE FROM keymetadata WHERE keyentryid IN {ids};",
f"DELETE FROM grant WHERE keyentryid IN {ids};",
f"DELETE FROM keyentry WHERE {where};"]
args = sys.argv[1:]
old = sqlite3.connect(f"file:{args.pop(0)}?mode=ro", uri=True)
replace = "--replace" in args
pairs = [tuple(map(int, a.split(":"))) for a in args if a != "--replace"]
# Older keystore2 schemas have no blobentry.state; every blob there is current (0).
blob_state = "state" if any(c[1] == "state" for c in old.execute("PRAGMA table_info(blobentry)")) else "0"
out = ["BEGIN;"]
for old_ns, new_ns in pairs:
if replace:
out += delete_keys(f"domain={DOMAIN_APP} AND namespace={new_ns}")
keys = old.execute("SELECT id, key_type, alias, state, km_uuid FROM keyentry WHERE domain=? AND namespace=?",
(DOMAIN_APP, old_ns)).fetchall()
for kid, key_type, alias, state, km_uuid in keys:
blobs = old.execute(f"SELECT id, subcomponent_type, blob, {blob_state} FROM blobentry WHERE keyentryid=?", (kid,)).fetchall()
if any(old.execute("SELECT 1 FROM blobmetadata WHERE blobentryid=? AND tag=?", (b[0], TAG_ENCRYPTED_BY)).fetchone()
for b in blobs):
print(f"-- skip {old_ns} {alias!r}: super-encrypted with the old lock-screen credential", file=sys.stderr)
continue
new_id = secrets.randbits(63) * (1 if secrets.randbits(1) else -1)
# The old key wins over one the app generated under the same alias on the new ROM.
out += delete_keys(f"domain={DOMAIN_APP} AND namespace={new_ns} AND alias={lit(alias)}")
out.append(f"INSERT INTO keyentry (id,key_type,domain,namespace,alias,state,km_uuid) VALUES "
f"({new_id},{key_type},{DOMAIN_APP},{new_ns},{lit(alias)},{state},{lit(km_uuid)});")
for bid, sub, blob, bstate in blobs:
out.append(f"INSERT INTO blobentry (subcomponent_type,keyentryid,blob,state) VALUES ({sub},{new_id},{lit(blob)},{bstate});")
for tag, data in old.execute("SELECT tag, data FROM blobmetadata WHERE blobentryid=?", (bid,)):
out.append(f"INSERT INTO blobmetadata (blobentryid,tag,data) VALUES ((SELECT max(id) FROM blobentry WHERE keyentryid={new_id}),{tag},{lit(data)});")
for tag, data, level in old.execute("SELECT tag, data, security_level FROM keyparameter WHERE keyentryid=?", (kid,)):
out.append(f"INSERT INTO keyparameter (keyentryid,tag,data,security_level) VALUES ({new_id},{tag},{lit(data)},{level});")
for tag, data in old.execute("SELECT tag, data FROM keymetadata WHERE keyentryid=?", (kid,)):
out.append(f"INSERT INTO keymetadata (keyentryid,tag,data) VALUES ({new_id},{tag},{lit(data)});")
print(f"-- {old_ns}->{new_ns} {alias!r}: {len(blobs)} blobs", file=sys.stderr)
out.append("COMMIT;")
print("\n".join(out))
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Cold-start an app's launcher activity the way Android 17 accepts from the shell. Usage: launch.sh PKG [WAIT] [SHOT.png]
p=$1 w=${2:-6} shot=${3:-}
act=$(adb shell "cmd package resolve-activity --brief -c android.intent.category.LAUNCHER $p" </dev/null | tr -d '\r' | tail -1)
adb shell "am start -S -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n '$act'" </dev/null >/dev/null
sleep "$w"
[ -n "$shot" ] && adb exec-out screencap -p > "$shot" </dev/null
true
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Snapshot phone health: memory, load, crashes/ANRs since boot, low-memory kills, log errors, cold launches.
# Usage: measure.sh OUT_FILE [launch-package ...]
set -uo pipefail
out=$1; shift
a() { adb shell "$@" </dev/null; }
{
echo "# $(date '+%F %T') $(a getprop ro.build.display.id) / Android $(a getprop ro.build.version.release)"
echo "## system"
a 'uptime; cat /proc/loadavg'
a 'grep -E "MemTotal|MemAvailable|Cached:|SwapTotal|SwapFree" /proc/meminfo'
a 'su -c "cat /proc/sys/vm/swappiness; cat /sys/block/zram0/mm_stat 2>/dev/null"'
echo "procs: $(a 'ls -d /proc/[0-9]* | wc -l')"
echo "## top cpu"
a 'top -b -n 1 -m 12 -o PID,USER,%CPU,%MEM,NAME' | tail -13
echo "## dropbox since boot"
boot=$(a 'date -d @$(( $(date +%s) - $(cut -d. -f1 /proc/uptime) )) "+%Y-%m-%d %H:%M:%S"')
a "dumpsys dropbox" | awk -v b="$boot" '$1" "$2 >= b && $3 ~ /anr|crash|watchdog|tombstone|wtf|lowmem|native/ {print $3}' | sort | uniq -c
echo "## crash buffer (FATAL / ANR per process)"
a 'logcat -d -b crash' | grep -oE "Process: [^,]+|FATAL EXCEPTION|>>> [^ ]+ <<<" | sort | uniq -c | sort -rn | head -20
a 'logcat -d -b events' | grep -oE "am_anr: \[[^]]*" | awk -F, '{print "ANR " $3}' | sort | uniq -c
echo "## lmkd / am_kill"
a 'logcat -d -b events' | grep -cE "am_kill|lmk_kill" | sed 's/^/kills: /'
a 'logcat -d -b events' | grep -oE "am_kill: \[[^]]*" | awk -F, '{print $5}' | sort | uniq -c | sort -rn | head -10
echo "## logcat error tags (top 25)"
a 'logcat -d *:E' | awk '{print $6}' | sort | uniq -c | sort -rn | head -25
echo "## thermal / battery"
a 'dumpsys battery | grep -E "level|temperature|status"; for z in /sys/class/thermal/thermal_zone*; do echo "$(cat $z/type) $(cat $z/temp)"; done 2>/dev/null | sort -k2 -n | tail -5'
if [ $# -gt 0 ]; then
echo "## cold launches (ms, am start -W -S, 3 runs)"
for p in "$@"; do
t=""
for _ in 1 2 3; do
act=$(a "cmd package resolve-activity --brief -c android.intent.category.LAUNCHER $p" | tr -d '\r' | tail -1)
v=$(a "am start -W -S -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n '$act'" | awk '/TotalTime/{print $2}')
t="$t ${v:-fail}"; a 'input keyevent KEYCODE_HOME'; sleep 2
done
echo "$p:$t"
done
fi
} 2>&1 | tee "$out"
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Rebuild the Nova home screen in Lawnchair, plus the Ice Box shortcuts Nova had pinned.
Usage: nova-to-lawnchair.py NOVA_DB LAWNCHAIR_DB OLD_ICEBOX_SHORTCUT_XML OUT_DIR
Writes OUT_DIR/launcher_5_5_5.db (Lawnchair schema, Nova's items), OUT_DIR/icebox.xml
(shortcut_service package file with only the pinned shortcuts) and OUT_DIR/lawnchair-pins.xml
(shortcut_service launcher file pinning them for Lawnchair).
"""
import re
import shutil
import sqlite3
import sys
from pathlib import Path
LAUNCHER = "app.lawnchair.nightly"
ICEBOX = "com.catchingnow.icebox"
# From /data/system/packages.xml on the new ROM: versionCode, lastUpdateTime, SHA-256 of the cert.
LAUNCHER_INFO = ("1600020100", "1792700106881", "dHw2RbNXJYsuI+hR5TyWdH/grdAH5bos2X6MhVcuTcU=")
ICEBOX_INFO = ("154821395", "1792699567277", "gvW+cC4DIBdGDKg3b22r1yWp5S3SSSDdXl17WF+/4bY=")
# Nova-only containers (gesture/dock-swipe slots); Lawnchair has nothing to put them in.
SKIP_CONTAINERS = {-207, -208}
ITEM_APP, ITEM_FOLDER, ITEM_WIDGET, ITEM_DEEP_SHORTCUT = 0, 2, 4, 6
FLAG_ID_NOT_VALID = 1 # Launcher3 restore flag: allocate and bind a fresh widget id on load
MISSING_PACKAGES = {"com.dot.gallery"} # not installed on the new ROM
# Google's smartspace widget on row 0 of the first page: Lawnchair reserves that row for its own.
SKIP_IDS = {56}
nova_db, lc_db, icebox_xml, out = map(Path, sys.argv[1:5])
out.mkdir(parents=True, exist_ok=True)
def package_info(version: str, updated: str, sig: str) -> str:
return (f'<package-info version="{version}" last_udpate_time="{updated}" shadow="0" allow-backup="1" '
f'allow-backup-initialized="1" bk_src_version="-1" bk_src_backup-allowed="0">\n'
f'<signature hash="{sig}" />\n</package-info>\n')
nova = sqlite3.connect(nova_db)
nova.row_factory = sqlite3.Row
rows = [r for r in nova.execute("select * from favorites where _id >= 0 order by _id")
if r["container"] not in SKIP_CONTAINERS and r["_id"] not in SKIP_IDS]
folders = {r["_id"] for r in rows if r["itemType"] == ITEM_FOLDER}
kept = []
for r in rows:
pkg = re.search(r"component=([\w.]+)/", r["intent"] or "")
if pkg and pkg.group(1) in MISSING_PACKAGES:
print(f"skip {r['title']}: {pkg.group(1)} not installed")
continue
if r["container"] >= 0 and r["container"] not in folders:
continue
kept.append(r)
db = out / "launcher_5_5_5.db"
shutil.copy(lc_db, db)
lc = sqlite3.connect(db)
lc.execute("delete from favorites")
pins = []
for r in kept:
widget = r["itemType"] == ITEM_WIDGET
hotseat = r["container"] == -101
in_folder = r["container"] >= 0
x, y = int(max(r["cellX"] or 0, 0)), int(max(r["cellY"] or 0, 0))
lc.execute(
"insert into favorites (_id,title,intent,container,screen,cellX,cellY,spanX,spanY,itemType,"
"appWidgetId,appWidgetProvider,restored,profileId,rank,options) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(r["_id"], r["title"], r["intent"], r["container"],
x if hotseat else (0 if in_folder else r["screen"]),
x, 0 if hotseat else y, int(r["spanX"]), int(r["spanY"]), r["itemType"],
r["appWidgetId"] if widget else -1, r["appWidgetProvider"],
FLAG_ID_NOT_VALID if widget else 0, 0, r["rank"], 0))
if r["itemType"] == ITEM_DEEP_SHORTCUT:
pins.append(re.search(r"S\.shortcut_id=([^;]+)", r["intent"]).group(1))
lc.commit()
print(f"{len(kept)} items written, pins: {pins}")
old = Path(icebox_xml).read_text()
blocks = [m.group(0) for m in re.finditer(r'<shortcut id="([^"]+)".*?</shortcut>\n', old, re.S) if m.group(1) in pins]
assert len(blocks) == len(pins), f"found {len(blocks)} of {len(pins)} shortcuts"
head = re.search(r"<package name=[^>]*>\n", old).group(0)
(out / "icebox.xml").write_text("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + head
+ package_info(*ICEBOX_INFO) + "".join(blocks) + "</package>\n")
(out / "lawnchair-pins.xml").write_text(
"<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
f'<launcher-pins package-name="{LAUNCHER}" launcher-user="0">\n' + package_info(*LAUNCHER_INFO)
+ f'<package package-name="{ICEBOX}" package-user="0">\n'
+ "".join(f'<pin value="{p}" />\n' for p in pins) + "</package>\n</launcher-pins>\n")
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Replace an app with a differently signed build while keeping its data, permissions,
# installer and frozen state. Data is also saved to BACKUP_DIR before the uninstall.
# Usage: reinstall-keep-data.sh PACKAGE BACKUP_DIR APK [APK ...]
set -euo pipefail
pkg=$1 bak=$2; shift 2
a() { adb shell "$@" </dev/null; }
su_() { a "su -c '$1'"; }
mkdir -p "$bak"
installer=$(a "pm list packages -3 -i $pkg" | tr -d '\r' | grep "package:$pkg " | sed 's/.*installer=//')
disabled=$(a "pm list packages -d $pkg" | tr -d '\r' | grep -cx "package:$pkg" || true)
perms=$(a "dumpsys package $pkg" | sed -n '/runtime permissions:/,/^ *[a-z]* .*:$/p' | grep -oE '[a-zA-Z0-9_.]+: granted=true' | cut -d: -f1 | sort -u | tr '\n' ' ')
echo "$pkg: installer=$installer disabled=$disabled perms=[$perms]"
dirs="/data/data/$pkg /data/user_de/0/$pkg"
[ "$(su_ "[ -d /data/media/0/Android/data/$pkg ] && echo y")" = y ] && dirs="$dirs /data/media/0/Android/data/$pkg"
su_ "am force-stop $pkg"
# The phone writes the tar to stdout; exec-out keeps it binary-safe.
adb exec-out "su -c 'tar -cf - --exclude=$pkg/cache --exclude=$pkg/code_cache $dirs 2>/dev/null'" > "$bak/$pkg.tar"
echo "saved $(stat -c %s "$bak/$pkg.tar") bytes to $bak/$pkg.tar"
[ "$(stat -c %s "$bak/$pkg.tar")" -gt 10240 ] || { echo "backup suspiciously small, aborting"; exit 1; }
a "pm uninstall $pkg"
iopt=(); [ -n "$installer" ] && [ "$installer" != null ] && iopt=(-i "$installer")
adb install-multiple -r "${iopt[@]}" --bypass-low-target-sdk-block "$@"
uid=$(su_ "stat -c %u /data/data/$pkg")
cache_gid=$((uid - 10000 + 20000))
adb exec-in "su -c 'tar -xf - -C /'" < "$bak/$pkg.tar"
su_ "while pgrep -x tar >/dev/null; do sleep 0.3; done"
for d in /data/data/$pkg /data/user_de/0/$pkg; do
su_ "chown -R $uid:$uid $d; for c in cache code_cache; do mkdir -p $d/\$c; chown $uid:$cache_gid $d/\$c; chmod 2771 $d/\$c; done; chcon -hR \"\$(stat -c %C $d)\" $d"
done
case " $dirs " in *Android/data*)
su_ "d=/data/media/0/Android/data/$pkg; chown -R $uid:1078 \$d; find \$d -type d -exec chmod 2770 {} +; find \$d -type f -exec chmod 660 {} +; chcon -R u:object_r:media_rw_data_file:s0 \$d" ;;
esac
for p in $perms; do a "pm grant $pkg $p" 2>/dev/null || true; done
[ "$disabled" = 1 ] && su_ "pm disable-user --user 0 $pkg"
echo "$pkg: reinstalled as uid $uid, $(a "dumpsys package $pkg" | grep -m1 versionName | xargs)"
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Restore user apps, app data and permissions onto a freshly flashed, rooted phone.
Sources (from the backup folder):
apks/<pkg>/*.apk APK + splits
rooted-fresh/dumpsys-package.txt installer, granted runtime permissions
rooted-fresh/packages-3.txt which packages are user apps
rooted-fresh/packages-disabled.txt frozen/disabled state
rooted/apps-manifest.json special app-ops (overlay, all-files, ...)
<work>/data/<pkg>, <work>/user_de/0/<pkg> unpacked data-fresh.tar
The device comes from ANDROID_SERIAL, as for adb. Usage: restore.py BACKUP_DIR WORK_DIR [--data-only] [pkg ...] (no pkgs = all user apps)
--data-only only put the data back (apps already installed and updated): keeps the current APK,
installer, permissions and frozen state. Never re-run a full restore over updated apps.
Every action and its result is appended to WORK_DIR/restore.log.
"""
import json
import re
import shlex
import subprocess
import sys
import time
from pathlib import Path
SKIP = {"com.topjohnwu.magisk"} # installed fresh, its data must not be reused
# Dropped on restore: regenerated by the app, and the bulk of most backups.
VOLATILE = ["cache", "code_cache", "app_webview/Default/Cache", "app_webview/Default/Code Cache"]
backup, work = Path(sys.argv[1]), Path(sys.argv[2])
data_only = "--data-only" in sys.argv[3:]
only = set(sys.argv[3:]) - {"--data-only"}
log_file = (work / "restore.log").open("a")
def log(msg: str) -> None:
line = f"{time.strftime('%H:%M:%S')} {msg}"
print(line, flush=True)
log_file.write(line + "\n")
log_file.flush()
def adb(*args: str, timeout: int = 300, stdin=None) -> subprocess.CompletedProcess:
return subprocess.run(["adb", *args], capture_output=True, text=stdin is None,
timeout=timeout, stdin=stdin)
def su(cmd: str, timeout: int = 300) -> str:
r = adb("shell", f"su -c {shlex.quote(cmd)}", timeout=timeout)
return (r.stdout + r.stderr).strip()
def parse_dumpsys(text: str) -> dict:
"""installer + granted runtime permissions per package."""
out = {}
for block in re.split(r"\n Package \[", text)[1:]:
name = block.split("]", 1)[0]
installer = re.search(r"\n installerPackageName=(\S+)", block)
runtime = block.split("runtime permissions:", 1)
perms = re.findall(r"\n ([\w.]+): granted=true", runtime[1]) if len(runtime) > 1 else []
out[name] = {"installer": installer.group(1) if installer else None, "perms": sorted(set(perms))}
return out
fresh = backup / "rooted-fresh"
meta = parse_dumpsys((fresh / "dumpsys-package.txt").read_text(errors="replace"))
manifest = json.loads((backup / "rooted/apps-manifest.json").read_text())["apps"]
user_pkgs = sorted(set(re.findall(r"=([\w.]+)\s+installer=", (fresh / "packages-3.txt").read_text())) - SKIP)
disabled = set(re.findall(r"package:([\w.]+)", (fresh / "packages-disabled.txt").read_text()))
targets = [p for p in user_pkgs if not only or p in only]
log(f"=== restore start: {len(targets)} packages")
# Pass 1: install every APK set, so installers like Obtainium exist before pass 2 names them.
for pkg in [] if data_only else targets:
apks = sorted(str(p) for p in (backup / "apks" / pkg).glob("*.apk"))
if not apks:
log(f"{pkg}: NO APK, skipped")
continue
# --bypass-low-target-sdk-block: Android 14+ refuses targetSdk < 24 (Automagic workarounds).
r = subprocess.run(["adb", "install-multiple", "-r", "-d", "-g",
"--bypass-low-target-sdk-block", *apks],
capture_output=True, text=True, timeout=900)
log(f"{pkg}: install {'OK' if 'Success' in r.stdout else 'FAILED ' + (r.stdout + r.stderr).strip()[-200:]}")
installed = set(re.findall(r"package:([\w.]+)", adb("shell", "pm list packages").stdout))
# Pass 2: data, ownership, SELinux labels, installer, permissions, disabled state.
for pkg in targets:
if pkg not in installed:
continue
info = meta.get(pkg, {})
su(f"am force-stop {pkg}")
uid = su(f"stat -c %u /data/data/{pkg}")
if not uid.isdigit():
log(f"{pkg}: no data dir after install ({uid}), data skipped")
continue
cache_gid = int(uid) - 10000 + 20000 # u0_aNNN_cache
members = [m for m in (f"data/{pkg}", f"user_de/0/{pkg}") if (work / m).is_dir()]
if members:
for m in members:
su(f"find /data/{m} -mindepth 1 -maxdepth 1 ! -name lib -exec rm -rf {{}} +")
excludes = [f"--exclude={m}/{v}" for m in members for v in VOLATILE]
tar = subprocess.Popen(["tar", "-cf", "-", "-C", str(work), *excludes, *members],
stdout=subprocess.PIPE)
r = adb("exec-in", "su -c 'tar -xf - -C /data'", stdin=tar.stdout, timeout=1800)
tar.wait()
# exec-in returns once our side is sent; the phone's tar may still be writing.
su("while pgrep -x tar >/dev/null; do sleep 0.3; done", timeout=600)
for m in members:
su(f"chown -R {uid}:{uid} /data/{m}; "
f"for d in cache code_cache; do mkdir -p /data/{m}/$d; chown {uid}:{cache_gid} /data/{m}/$d; "
# restorecon gives only app_data_file:s0; the per-app MLS categories that
# installd put on the top directory must be copied onto its contents.
f"chmod 2771 /data/{m}/$d; done; chcon -hR \"$(stat -c %C /data/{m})\" /data/{m}",
timeout=900)
wrong = su(f"find {' '.join('/data/' + m for m in members)} ! -user {uid} | wc -l")
log(f"{pkg}: data {'+'.join(members)} rc={r.returncode} uid={uid} wrong_owner={wrong}")
else:
log(f"{pkg}: no data in backup")
if data_only:
continue
inst = info.get("installer")
if inst and inst != "null" and inst in installed | {"com.android.vending"}:
log(f"{pkg}: installer {inst} -> {su(f'pm set-installer {pkg} {inst}') or 'OK'}")
# -g granted everything at install; revoke what was not granted before.
wanted = set(info.get("perms", []))
now = re.findall(r"\n\s+([\w.]+): granted=true", su(f"dumpsys package {pkg} | sed -n '/runtime permissions:/,$p'"))
revoked = [p for p in set(now) - wanted]
for p in revoked:
su(f"pm revoke {pkg} {p}")
for p in wanted - set(now):
su(f"pm grant {pkg} {p}")
ops = manifest.get(pkg, {}).get("specialAppOps", {})
for op, mode in ops.items():
su(f"appops set {pkg} {op} {mode}")
log(f"{pkg}: perms granted={len(wanted)} revoked={len(revoked)} appops={','.join(ops) or '-'}")
if pkg in disabled:
log(f"{pkg}: disable-user -> {su(f'pm disable-user --user 0 {pkg}')}")
log("=== restore done")
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Launch every user app with a launcher activity, one by one: unfreeze if frozen, cold start,
# wait, screenshot, collect its crash/ANR/keystore log lines, check it is alive and in front,
# then stop it and re-freeze. Writes OUT_DIR/results.tsv, OUT_DIR/<pkg>.png, OUT_DIR/<pkg>.log.
# Usage: test-all-apps.sh OUT_DIR [pkg ...]
set -uo pipefail
out=$1; shift
mkdir -p "$out"
a() { adb shell "$@" </dev/null; }
WAIT=${WAIT:-8}
if [ $# -gt 0 ]; then pkgs=("$@"); else
mapfile -t pkgs < <(a 'pm list packages -3' | tr -d '\r' | sed 's/package://' | sort)
fi
frozen=$(a 'pm list packages -d' | tr -d '\r' | sed 's/package://')
printf 'pkg\twas_frozen\tlaunch_ms\talive\tforeground\tcrash\tanr\tkeystore\tnote\n' > "$out/results.tsv"
for p in "${pkgs[@]}"; do
wf=0; grep -qx "$p" <<<"$frozen" && wf=1
[ $wf = 1 ] && a "su -c 'pm enable --user 0 $p'" >/dev/null
act=$(a "cmd package resolve-activity --brief -c android.intent.category.LAUNCHER $p" | tr -d '\r' | tail -1)
if [[ "$act" != */* ]]; then
[ $wf = 1 ] && a "su -c 'pm disable-user --user 0 $p'" >/dev/null
printf '%s\t%s\t-\t-\t-\t-\t-\t-\tno launcher activity\n' "$p" "$wf" >> "$out/results.tsv"; continue
fi
a 'logcat -c'
ms=$(a "am start -W -S -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n '$act'" | awk '/TotalTime/{print $2}')
sleep "$WAIT"
adb exec-out screencap -p > "$out/$p.png" </dev/null
pid=$(a "pidof $p" | tr -d '\r')
fg=$(a 'dumpsys activity activities' | grep -m1 -E 'topResumedActivity|mResumedActivity' | grep -c "$p/")
a 'logcat -d' > "$out/$p.log"
crash=$(grep -cE "FATAL EXCEPTION|Process: $p|Fatal signal" "$out/$p.log")
anr=$(grep -cE "ANR in $p" "$out/$p.log")
ks=$(grep -ciE "AEADBadTag|KeyStoreException|VERIFICATION_FAILED|KeyPermanentlyInvalidated|UnrecoverableKey|BadPaddingException|file is not a database|SQLiteDatabaseCorrupt" "$out/$p.log")
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n' "$p" "$wf" "${ms:-?}" "${pid:+yes}" "$fg" "$crash" "$anr" "$ks" >> "$out/results.tsv"
echo "$p frozen=$wf ms=${ms:-?} alive=${pid:+yes} fg=$fg crash=$crash anr=$anr ks=$ks"
a "am force-stop $p; input keyevent KEYCODE_HOME"
[ $wf = 1 ] && a "su -c 'pm disable-user --user 0 $p'" >/dev/null
done
+201
View File
@@ -0,0 +1,201 @@
---
name: software-vision
description: >-
Use this skill whenever the user describes an idea for a new piece of software, a tool, an app,
a service, a bot, a plugin, a mod, or an automation — "I want something that …", "build me a …",
"could we make a tool for …", "I need a service that …" — before choosing a language, scaffolding
a repository, or writing any code. Requires prior-art research first: web searches for existing
projects, forks, skeletons, libraries and what users say about them (reviews, complaints,
comparisons), only then merged with training-data recall as a secondary source, a
throwaway shallow clone of promising candidates to check them against the requirements, and a
set of options (fork, depend on, extend, start fresh) for the user to choose from.
---
# Software Vision — Find What Exists Before Building
When the user has an idea for a piece of software, the first job is **not** to design it. It is
to find out what already exists that does all or most of it. Building a full stack from nothing
when a project covering 80% of the requirements is one `git clone` away wastes days, and the
result is usually worse than the thing that already had years of bug reports behind it.
This is the project-level form of the dependency ordering in
[`coding-dev.md` §3](skills-public/coding-dev.md): use what exists, fork what is close, write
your own only when nothing fits. That section governs individual subsystems; this one governs
the whole idea.
> [!IMPORTANT]
> **Never assume nothing exists.** However niche the idea sounds, someone may have built it,
> half-built it, abandoned it, drafted a skeleton, written a blog post about the protocol, or
> published research on the hard part. "I couldn't think of anything" is not a finding — a list
> of the searches you ran and what they turned up is.
---
## 1. When This Applies — and the Only Opt-Out
Run this whole process whenever the user describes software they want to exist: a new app,
CLI, service, bot, plugin, mod, integration, library, or automation — whether it is a fresh
repository or a large new component inside an existing one.
**The only way to skip it is an explicit opt-out from the user**, such as "no online research",
"don't look at other projects", or "I want to write this from scratch myself". Then:
- Skip the web searches and the clones (§3–§4).
- Still use the libraries rule in [`coding-dev.md` §3](skills-public/coding-dev.md) for
individual subsystems unless the user excluded that too.
- Do not argue for research afterwards; the user has already decided.
Urgency, a small-sounding scope, or "it's just a quick script" are **not** opt-outs. A few
searches take a minute; reinventing a maintained tool takes far longer.
---
## 2. Pin Down the Requirements First
Before searching, turn the user's description into a short checklist, because that list is what
every candidate gets measured against:
- **Must-haves** — what the software has to do to be useful at all.
- **Nice-to-haves** — features the user mentioned that are not deal-breakers.
- **Constraints** — platform (Linux, Windows, Wine, Android, a game engine), runtime, language
preferences, self-hosted vs cloud, license needs, offline use, performance.
Show the checklist to the user with the results (§6) so they can correct it. If a requirement
is genuinely ambiguous *and* would change which candidates qualify, ask — otherwise state the
assumption and carry on.
---
## 3. Search First, Then Merge in Recall
The searches happen anyway, so they come **first**, and the decision rests on what they show.
Training data can be months or years out of date: projects get abandoned, renamed, relicensed,
or overtaken, and newer ones may never have been seen at all. Starting from recall anchors the
whole evaluation on a stale picture, so recall comes second and only fills gaps.
### 3.1 Web searches first
**Run at least three to five searches**, varied in phrasing and source, before settling on any
candidate. Cover:
| Where | What it finds |
| :--- | :--- |
| General web search with the problem phrased several ways ("self-hosted X", "open source alternative to Y", "X for Linux") | Products, blog posts, comparisons |
| GitHub / GitLab / Codeberg search, topics, and `awesome-*` lists | Projects, forks, skeletons, abandoned prototypes |
| The package registry for the likely language (crates.io, npm, PyPI, NuGet, pkg.go.dev, …) | Libraries that do the hard part |
| Forums and communities (Reddit, Hacker News, the domain's own Discord/forum, issue trackers of adjacent projects) | "Does anything do X?" threads, half-finished attempts, known dead ends |
| Papers, specs, reverse-engineering notes for niche domains | The research behind the hard part, even if nobody shipped an app |
Search for **parts** as well as the whole: an idea with no complete match often splits into
pieces that each have a mature library or tool.
For each promising hit, note what can be checked without cloning: last commit or release date,
license, language, open issue count and how maintainers respond, stars or downloads as a weak
signal, and whether its README claims the must-haves.
### 3.2 What people say about it
A project's own README describes what it *claims* to do. What users say shows how it actually
holds up, so search for opinions on every serious candidate too: reviews, "X vs Y" comparisons,
Reddit and Hacker News threads, forum posts, and the complaints in its issue tracker. Look for:
- Recurring pain points (crashes, data loss, missing features people keep requesting)
- Whether people are moving to it or away from it, and to what
- How the maintainers respond to bugs and pull requests
- Workarounds people rely on, which often point at the gaps a fork would need to fill
Weight recent opinions over old ones, and many independent voices over a single loud one. Put
this in the comparison (§6) next to the technical facts.
### 3.3 Then merge in recall
Only after searching, add what you already know from training data: projects, products,
libraries, and standards that the searches missed. Every recalled item must be checked with a
quick search before it goes into the comparison (does it still exist, is it maintained, has the
license changed?). Where recall and current sources disagree, **current sources win.**
---
## 4. Clone the Promising Candidates — Then Delete Them
When a candidate looks close but it is unclear whether it ticks every must-have, **look inside
it** rather than guessing from the README.
1. **Shallow-clone into a throwaway directory** — the session's scratchpad or a fresh `mktemp -d`,
never inside a project tree:
```bash
dir=$(mktemp -d) && git clone --depth 1 <url> "$dir/<name>"
```
2. **Read, don't run.** A clone is untrusted code. Inspect the README, directory layout, entry
points, dependency manifest, build files, tests, and open issues. Do not run its install
scripts, build, or test suite as part of evaluation — that is a decision for after the user
picks it.
3. **Map it against the checklist from §2**: which must-haves it covers, which are partial, which
are missing, and roughly how much work each gap is. Note the architecture (does it have a
plugin/extension point where the gaps could go?), code quality, and the license.
4. **Delete the clone when done.** Nothing from evaluation stays on disk or in a project. If the
user later chooses to fork it, that is a fresh, deliberate clone or fork in the right place.
Clone only the few candidates that are genuinely in contention, not every search hit.
---
## 5. Pick Likely Libraries
Whether the plan ends up as a fork or a fresh start, identify the libraries the work would lean
on, for the language that fits best (the existing candidate's language if forking, otherwise the
best fit for the constraints and the user's preferences). For each major subsystem — protocol,
file format, UI toolkit, storage, networking, parsing — name one or two maintained candidates
and why. The per-language rules under
[`code-languages/`](skills-public/code-languages/) apply to whatever is chosen.
---
## 6. Present Options — the User Chooses
Report back **before** scaffolding or writing code, with:
1. **The requirements checklist** (§2), so the user can correct it.
2. **What was searched** — the queries and sources, briefly, so an empty result is credible.
3. **A comparison of the real candidates**:
| Candidate | Covers | Missing / partial | License | Health | User opinion | Fit |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
4. **Options**, typically some of:
- **Use it as is** — it already does the job; maybe configure or script around it.
- **Extend it** — via its plugin/extension API, with no fork.
- **Fork and modify** — close, but the gaps need changes to its core.
- **Contribute upstream** — the gaps are useful to everyone; send them as PRs (then the
project's own rules win, per the precedence table in
[`coding-dev.md`](skills-public/coding-dev.md)).
- **Combine** — several existing tools or libraries glued together.
- **Start fresh** — on top of the libraries from §5, with a short, explicit reason why none of
the candidates were a usable base.
5. **A recommendation**, with the reason — but the decision is the user's. Do not quietly start
on the recommended option. When the user has said to work autonomously and decide without
them, take the recommendation and record the choice and the alternatives where they will see
it.
If nothing usable exists, say so plainly, list what was checked, and still mention near misses
worth borrowing ideas, formats, or code from (license permitting).
**Keep hosting out of the proposal.** The options are about *what software* to run, not how to
deploy it. Containers, proxies, tunnels, extra helper services, and the database layout belong to
the environment's own infrastructure skill, which gets read once an option is chosen and before
anything is deployed. Note a hard requirement that affects the choice (it needs Postgres, it
needs a GPU) and stop there.
---
## 7. Checklist
- [ ] Requirements turned into must-haves / nice-to-haves / constraints
- [ ] At least three to five varied web searches first, covering the whole idea and its parts
- [ ] User reviews, comparisons, and complaints gathered for each serious candidate
- [ ] Training-data recall merged in afterwards, each item checked against current sources
- [ ] Promising candidates shallow-cloned to a temp directory, read without running, mapped
against the checklist, then deleted
- [ ] Libraries identified for the likely language
- [ ] Options and a recommendation presented; nothing built before the user chose (or before
an autonomous decision was recorded)