Skip to content

Commit 26d4100

Browse files
authored
General: Move on-demand agent docs out of the always-loaded rules (#37)
Finishes the restructuring #36 landed half of: rules/release.md was renamed to skills/release/SKILL.md there as a pure rename, so it had been sitting in main without YAML frontmatter and could not load as a skill. Three bodies of reference now live in .claude/skills/, each with frontmatter, and the always-loaded rules keep only what every session needs: - release — regains its frontmatter, and takes back the screenshot workflow that build-commands.md was duplicating - device-qualification — qualification protocol, verified-device ledger and per-OEM known gaps, out of privileged-access.md - oem-adapters — per-OEM adapter detail (keys, value domains, write ordering, session overrides), out of architecture.md Content is unchanged in the move. architecture.md goes 208 -> 95 lines and privileged-access.md 231 -> 136; each keeps a pointer to its skill, and CLAUDE.md states the no-nested-CLAUDE.md rule explicitly so the adapter detail is not re-nested next to the code later. Source comments that pointed at the ledger in .claude/rules/privileged-access.md now point at the skill. Comment-only; compileFossDebugKotlin passes. The CLAUDE.md status line claimed 0.1.0-beta1 and now defers to VERSION.
1 parent 6dba6d5 commit 26d4100

12 files changed

Lines changed: 272 additions & 252 deletions

File tree

.claude/CLAUDE.md

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,20 @@ gated to a **physically-qualified device-codename allowlist** (HAL enforcement i
2121
system user; **reads are unprivileged (ContentResolver), writes require Shizuku** (the shell UID holds
2222
`lineageos.permission.WRITE_SETTINGS`, which `WRITE_SECURE_SETTINGS` does not cover). Other Pixels, Samsung on
2323
unverified One UI versions (6/7, 9+), non-HyperOS-2 Xiaomi devices, non-ColorOS-15 Oplus devices, and unqualified
24-
LineageOS builds remain diagnostics-only. See the qualification ledger in `.claude/rules/privileged-access.md` for
24+
LineageOS builds remain diagnostics-only. See the qualification ledger (`.claude/skills/device-qualification/`) for
2525
the verified devices and mappings.
2626

27-
Package: `eu.darken.amply`. License: GPL-3.0-or-later. Status: pre-launch (`0.1.0-beta1`).
27+
Package: `eu.darken.amply`. License: GPL-3.0-or-later. Status: pre-launch (current version in `VERSION`).
2828

2929
## Project Shape
3030

31-
- **Single Gradle module**: `:app` (declared in `settings.gradle.kts`). This is *not* a multi-module project.
32-
- **Product flavors** (`distribution` dimension): `foss` and `gplay`.
33-
- **Build types**: `debug`, `beta` (minified), `release` (minified). Every variant shares the single applicationId
34-
`eu.darken.amply` — no build-type suffixes; installed variants are mutually exclusive (different signing keys).
35-
- **SDKs**: `compileSdk`/`targetSdk` 36, `minSdk` 26. Core-library desugaring enabled.
31+
Single Gradle module (`:app`), flavors and build types are declared in `app/build.gradle.kts`. Two non-obvious
32+
constraints:
33+
3634
- **Java**: build/test toolchain needs **JDK 21** (Robolectric requires it to emulate Android SDK 36); compiled
3735
bytecode still targets **Java 17** (`compileOptions`/`jvmTarget` in `app/build.gradle.kts`).
38-
- **Stack**: Kotlin, Jetpack Compose + Material 3, Navigation3, Glance (widget), Hilt/KSP, Coroutines/Flow,
39-
Preferences DataStore, Shizuku (AIDL user service).
36+
- **Every variant shares the single applicationId** `eu.darken.amply` — no build-type suffixes. Because signing
37+
certificates differ, installed variants are mutually exclusive on a device; switching requires an uninstall.
4038

4139
## Package Layout (feature/core/ui)
4240

@@ -45,7 +43,8 @@ Under `app/src/main/java/eu/darken/amply/`:
4543
- `charging/core` — policies, device capability checks, OEM adapters, WSS, Shizuku access (`access/shizuku`, `adapter`)
4644
- `fullcharge/core` — temporary sessions, boot recovery, reconnect gesture
4745
- `main/ui` — activity, onboarding, dashboard, settings, setup guide, `tile`, `widget`
48-
- `diagnostics/core` + `diagnostics/ui` — privileged settings comparison and its guided UI
46+
- `diagnostics/core` + `diagnostics/ui` — "Help add support" contribution wizard: read-only multi-mode setting
47+
discovery + on-device privacy review
4948
- `common` — shared DataStore owner (`AppDataStore`) and cross-feature primitives
5049
- `common/datastore` — the `createValue()` settings DSL every preference facade is built on (`DataStoreValue`)
5150
- `common/serialization` — the single `Json` plus `ChargePolicySerializer`, for JSON-backed setting records
@@ -67,18 +66,24 @@ AIDL boundary: `app/src/main/aidl/eu/darken/amply/charging/core/access/shizuku/I
6766

6867
## Rules
6968

70-
Topic-specific guidance lives in `.claude/rules/`:
69+
Always-loaded topic guidance lives in `.claude/rules/`:
7170

72-
- `architecture.md`package layout, data flow, `ChargeObservation`, adapters, session/recovery, privileged boundary
73-
- `privileged-access.md` — Shizuku/WSS access paths, capability gate, AIDL safety boundary (read before touching control code)
71+
- `architecture.md` — data flow, `ChargeObservation`, session/recovery, reconnect gesture, pitfalls
72+
- `privileged-access.md` — Shizuku/WSS access paths, capability gates, AIDL safety boundary (read before touching control code)
7473
- `build-commands.md` — gradle build/test/lint commands, flavors, build types
7574
- `code-style.md` — Kotlin/Compose conventions, logging, DataStore
7675
- `testing.md` — JUnit 5 + Kotest conventions (JUnit 4 only for Robolectric)
7776
- `commit-guidelines.md` — commit/PR format and prefixes
7877
- `localization.md` — string extraction conventions and the current gap
79-
- `release.md` — versioning, signing, CI (pre-launch state)
8078
- `agent-instructions.md` — sub-agent usage and working principles
8179

80+
Loaded on demand, as skills (`.claude/skills/`) — there are **no nested `CLAUDE.md` files** in this repo, all
81+
guidance lives under `.claude/`:
82+
83+
- `oem-adapters` — per-OEM adapter detail (keys, value domains, write ordering, session overrides)
84+
- `device-qualification` — physical qualification protocol, verified-device ledger, per-OEM known gaps
85+
- `release` — versioning, `bump.sh`, signing, release workflows, store metadata + screenshots
86+
8287
## Safety Boundary (read first)
8388

8489
Amply has **no arbitrary shell API**. The Shizuku user service executes argument-separated commands, validates

.claude/rules/agent-instructions.md

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,16 @@ Per the global rules, prefer these for this Kotlin/Android project:
1616
- **`debugbadger`** tools/agent for on-device use-case runs and logcat capture (Android device automation).
1717
- **`Explore`** / **`general-purpose`** for broad codebase searches when you only need the conclusion.
1818

19-
## When to use sub-agents
20-
21-
**Use for:** exploring unfamiliar parts of the codebase, cross-file pattern searches, multi-file research, parallel
22-
work. **Handle directly:** known-path reads, single-file edits with clear requirements, quick grep/glob.
23-
2419
## Reading before changing control code
2520

2621
Before editing charge-control, Shizuku/WSS access, the AIDL, or the capability gate, read `privileged-access.md` and
2722
the relevant part of `architecture.md`. These paths have real safety constraints (allowlist, no shell strings,
28-
capability gate) that must not be relaxed casually.
29-
30-
## Multi-step work
31-
32-
1. Break complex tasks into discrete steps (use TaskCreate/TaskUpdate to track).
33-
2. Complete and verify one step before the next.
34-
3. Separate exploring (read-only) from implementing (minimal, focused edits) — when uncertain, explore first.
35-
36-
## Error handling
37-
38-
Understand a tool failure before retrying; don't repeat a failing approach. Report blockers early rather than working
39-
around them silently. Ask for clarification on ambiguous requirements via the AskUserQuestion tool.
23+
capability gate) that must not be relaxed casually. For adapter internals (keys, value domains, write ordering), use
24+
the `oem-adapters` skill.
4025

4126
## Device testing
4227

4328
Follow the global Test-Target rules: never adopt an Android device/emulator you didn't start unless interference is
4429
positively ruled out, and never re-point a named target without confirmation. Amply's control paths are
45-
capability-gated to specific Pixels — record device results in the qualification ledger in `privileged-access.md`
30+
capability-gated to specific Pixels — record device results in the qualification ledger (`device-qualification` skill)
4631
rather than loosening the gate to run on an unqualified device.

.claude/rules/architecture.md

Lines changed: 7 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,7 @@ project's convention. Code is grouped by **feature**, not framework layer.
1010

1111
## Package Map
1212

13-
```
14-
eu.darken.amply
15-
├── charging/core policies, capability gate, OEM adapters, WSS + Shizuku access
16-
│ ├── access/shizuku Shizuku detection, user-service client, AIDL boundary
17-
│ └── adapter AdapterRegistry + adapters (Pixel, Samsung, Xiaomi, OnePlus/ColorOS, LineageOS live)
18-
├── fullcharge/core temporary session, boot recovery, reconnect gesture, decision engines
19-
├── diagnostics/core + ui "Help add support" contribution wizard: read-only multi-mode setting discovery + on-device privacy review
20-
├── main
21-
│ ├── core app-level wiring
22-
│ └── ui MainActivity, onboarding, dashboard, settings, setup, tile, widget
23-
└── common
24-
├── AppDataStore single process-safe Preferences DataStore owner
25-
├── theming brand / Material You / mode / contrast prefs
26-
├── settings reusable hierarchical settings rows + sections
27-
└── debug/logging Logging fan-out + backends (Logcat, File)
28-
```
13+
See "Package Layout" in `.claude/CLAUDE.md`.
2914

3015
Feature-specific preference facades live with their owning feature but share the one `AppDataStore` instance. They
3116
declare their settings with the `createValue()` DSL (`common/datastore`) rather than touching `store.data` — with a
@@ -60,95 +45,11 @@ sticky broadcast keeps its last powered value, so hardware state is never treate
6045
to the last request. Normal (`1`) stays unknown without Shizuku (inactive adaptive vs. unrestricted are
6146
indistinguishable).
6247

63-
## Samsung Adapters
64-
65-
Two live adapters, gated by `ro.build.version.oneui` ranges plus `protect_battery` presence plus system user
66-
(all in world-readable `global` namespace; only writes need WSS):
67-
68-
- **Modern (One UI 8.x)**: `protect_battery` 0=off / 1=Maximum / 3=Standard(pause at 100%, resume 95%), plus
69-
`battery_protection_threshold` 80|85|90|95 (absent = 80, only valid ticks decode; malformed → Unknown).
70-
Policies: FixedLimit(80/85/90/95), PauseAtFull, Unrestricted. Session override = **PauseAtFull** (reaches 100%
71-
while keeping Samsung's own safety net). Threshold is written before mode.
72-
- **Legacy (One UI 4.x/5.x)**: `protect_battery` 0/1 toggle, fixed 85% cap. Policies: FixedLimit(85),
73-
Unrestricted. Session override = Unrestricted.
74-
75-
Writes apply **synchronously** (`VerificationStrategy.SYNC_READBACK`): `apply()` requires read-back equality, no
76-
pending-settle window, boot recovery converges on settings readback, and no reapply-inversion trick is needed.
77-
The reconnect gesture is Pixel-only (`reconnectGestureSupported`). One UI 6/7 and 9+ fall through to the
78-
diagnostics-only lab adapter. An external `protect_battery=0` makes One UI forget the user's prior mode (it falls back
79-
to the OEM default on re-enable), so Amply restores the exact prior policy itself rather than trusting Samsung's
80-
bookkeeping. Verified devices + coverage: see the qualification ledger in `privileged-access.md`.
81-
82-
## Xiaomi Adapter
83-
84-
One live adapter (`xiaomi-hyperos2-v1`), gated to the HyperOS ROM version (the setting is a ROM
85-
feature, not a per-model one): manufacturer Xiaomi (covers Redmi/POCO — they report Xiaomi as
86-
manufacturer) + `ro.mi.os.version.code == 2` (HyperOS 2.x) + system user. Use `ro.mi.os.version.code`,
87-
NOT the frozen legacy `ro.miui.ui.version.code`. Single key
88-
`secure/security_pc_secure_protect_mode_key`: `0`=charge fully, `1`=Intelligent (heuristic 80% hold →
89-
`ChargePolicy.Adaptive`), absent=Intelligent (factory state). No hard-cap mode exists. SYNC_READBACK
90-
with read-back equality; session override = Unrestricted; protective default = Adaptive. HyperOS 1,
91-
pre-HyperOS MIUI, and a future HyperOS 3 fall to `XiaomiLabAdapter` (diagnostics + contribution). Two
92-
documented assumptions: the feature is treated as present on any HyperOS 2 device (a device lacking it
93-
reads the key absent → a harmless false claim of control), and daemon-level enforcement of external
94-
writes is pending long-term observation (see the qualification ledger in `privileged-access.md`).
95-
96-
## OnePlus / ColorOS Adapter
97-
98-
One live adapter (`oplus-coloros15-v1`) for the ColorOS/OxygenOS (Oplus) family — OnePlus, Oppo, Realme —
99-
gated to `ro.build.version.oplusrom == 15` (Oplus-exclusive property, so it doubles as the family signal) +
100-
system user. Two mutually-exclusive **`system`** keys under Battery health: `regular_charge_protection_switch_state`
101-
= "Charging limit" (fixed 80% cap → `FixedLimit(80)`) and `smart_charge_protection_switch_state` = "Smart charging"
102-
(adaptive → `Adaptive`); neither on = Unrestricted; both on = Unknown/unrecognized. The OEM enforces exclusion and
103-
keeps a `_status` mirror (Amply writes only `_switch_state`). SYNC_READBACK with read-back equality; session
104-
override = Unrestricted; protective default = FixedLimit(80). **Writes require Shizuku** — the keys are `system`
105-
namespace, which WRITE_SECURE_SETTINGS cannot write (reads are unprivileged); the adapter sets
106-
`preferShizukuForWrites`. Unqualified Oplus versions fall to `OnePlusLabAdapter`. Enforcement is directly
107-
observable (device holds at 80%). See the qualification ledger in `privileged-access.md`.
108-
109-
## LineageOS Adapter
110-
111-
One live adapter (`lineageos-chargingcontrol-v1`) plus a `LineageLabAdapter`, for LineageOS's native Charging
112-
Control. **Manufacturer-agnostic** — the ROM changes charging control regardless of the OEM hardware — so both are
113-
registered **first** in `AdapterRegistry`, ahead of every OEM adapter; a LineageOS build on Samsung/Xiaomi/OnePlus/
114-
Pixel hardware is handled by these, never the OEM lab adapters (stock devices have `lineageOsVersion == null` and
115-
skip both). Gate: `ro.lineage.build.version` present + `Build.DEVICE` in a **physically-qualified codename
116-
allowlist** (`QUALIFIED_CODENAMES`) + `lineagesettings` provider present + system user. Unqualified LineageOS builds
117-
fall to `LineageLabAdapter`.
118-
119-
The three keys live in the private `content://lineagesettings/system` provider (`SettingNamespace.LINEAGE_SYSTEM`),
120-
NOT any AOSP `settings` namespace: `charging_control_enabled` (0/1), `charging_control_mode` (3=LIMIT), and
121-
`charging_control_charging_limit` (the discrete ticks 70/75/80/85/90/95). A hard cap is `enabled=1`+`mode=3`+`limit=N`;
122-
`enabled=0` is Unrestricted. Writes are ordered limit→mode→enabled (the observable "on" flip last). **Reads are
123-
unprivileged** (`LineageSettingsClient` via ContentResolver, shared by both backends); **writes require Shizuku**
124-
(`content insert`; the shell UID holds `lineageos.permission.WRITE_SETTINGS`, which `WRITE_SECURE_SETTINGS` cannot
125-
cover — `preferShizukuForWrites`, and the WSS auto-grant is skipped). `SYNC_READBACK` with read-back equality;
126-
session override = Unrestricted; protective default = FixedLimit(80); reconnect gesture unsupported.
127-
128-
LineageOS's own `ChargingControlController` observes these keys and re-drives the `vendor.lineage.health.
129-
IChargingControl` HAL, so an external write is honored. But the HAL is **device-dependent** (the setting can flip
130-
while charging never actually stops — the `mIsLimitSet:false` bug), which is why the gate is a qualified-codename
131-
allowlist and control ships disabled until a codename is physically proven. `read()` returns `Verified` **only** for
132-
states v1 can restore exactly (a supported fixed limit, or Unrestricted); AUTO/CUSTOM schedules, off-tick limits, and
133-
an absent/malformed `enabled` decode to `Unknown(unrecognizedValue=true)` so a temporary session refuses rather than
134-
clobbering the user's native choice. Verified devices + coverage: see the qualification ledger in `privileged-access.md`.
135-
136-
## Pixel Adapter
137-
138-
Writes **only** two secure settings:
139-
140-
- `secure/adaptive_charging_enabled`
141-
- `secure/charge_optimization_mode`
142-
143-
Ordering matters:
144-
145-
- Fixed 80%: adaptive `0`, then mode `1`
146-
- Unrestricted: mode `0`, then adaptive `0`
147-
- Adaptive: mode `0`, then adaptive `1`
148-
149-
Google's Settings Intelligence worker applies external secure-setting changes **asynchronously** (measured
150-
charging-HAL delay ≈ 11–12 s on tested Pixels). A same-package same-value write does **not** fire the settings
151-
observer — re-writes briefly invert `charge_optimization_mode` before applying the target.
48+
## OEM Adapters
49+
50+
Per-adapter detail (Samsung, Xiaomi, OnePlus/ColorOS, LineageOS, Pixel — keys, value domains, write ordering,
51+
session overrides) lives in the **`oem-adapters` skill** — read it before changing anything under
52+
`charging/core/adapter`.
15253

15354
## Temporary Session & Recovery
15455

@@ -190,5 +91,5 @@ Android does not deliver `ACTION_POWER_CONNECTED` / `ACTION_POWER_DISCONNECTED`
19091
replace this runtime gate with an exact-model allowlist or a version-only check.
19192
- Shizuku installation is detected by resolving the owner of `ShizukuProvider.PERMISSION`, **not** a fixed package
19293
name — this recognizes renamed forks and hidden-package mode. Don't hardcode a package name.
193-
- Pixel/Samsung/Xiaomi/Oplus keys are all live on gated devices (see the adapter sections). New writable keys must
94+
- Pixel/Samsung/Xiaomi/Oplus keys are all live on gated devices (see the `oem-adapters` skill). New writable keys must
19495
be spike-verified and added to `SettingWritePolicy` with an explicit per-key value domain.

.claude/rules/build-commands.md

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ wrapper (`./gradlew`). Build/test on **JDK 21** (Robolectric needs it for Androi
1111
./gradlew assembleFossDebug
1212
./gradlew assembleGplayDebug
1313

14-
# Release APKs (minified; signed when signing material is present, unsigned otherwise — see release.md)
14+
# Release APKs (minified; signed when signing material is present, unsigned otherwise — see the `release` skill)
1515
./gradlew assembleFossRelease
1616
./gradlew assembleGplayRelease
1717

@@ -42,23 +42,7 @@ bash fastlane/check_metadata_length.sh
4242

4343
## Play Store Screenshots
4444

45-
Screenshots are rendered from `@Preview` composables on the JVM (no device) via the Compose Preview Screenshot
46-
Testing plugin (`com.android.compose.screenshot`, enabled by `android.experimental.enableScreenshotTest=true` in
47-
`gradle.properties`). The store composables live in `app/src/debug/.../screenshots/ScreenshotContent.kt`; the capture
48-
entry points (`@PreviewTest`) and locale annotations live in `app/src/screenshotTest/.../screenshots/`.
49-
50-
```bash
51-
# 1. Render (writes to app/src/screenshotTestGplayDebug/reference/, which is gitignored)
52-
./fastlane/generate_screenshots.sh
53-
# 2. Normalize (flatten alpha → opaque 1080x1920) + sort into the committed metadata tree
54-
./fastlane/copy_screenshots.sh
55-
```
56-
57-
Committed output lands in `fastlane/metadata/android/en-US/images/phoneScreenshots/` as `1_dashboard_light.png …
58-
6_reconnect_gesture.png` (names come from `copy_screenshots.sh`'s `screen_file` map). Both scripts fail
59-
loudly on any count/dimension/format mismatch and `copy_screenshots.sh` requires ImageMagick. Needs the JDK 21 build
60-
toolchain like everything else. CI compiles these sources (`compileGplayDebugScreenshotTestKotlin`) but does **not**
61-
render — layoutlib output differs across machines — so **regenerating screenshots is a manual pre-release step**.
45+
Rendered from `@Preview` composables on the JVM — see the `release` skill.
6246

6347
## Install & Inspect
6448

0 commit comments

Comments
 (0)