Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/lint-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ on:
- "id_registry.csv"
- "scripts/lint_docs.py"

# Least privilege for the job's GITHUB_TOKEN (added 2026-10-15, ai-sessions/0094 — ai-sessions/0093 A93-APP-09): it only reads the repository.
permissions:
contents: read

jobs:
lint-docs:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
**/.claude/

# Android / Gradle build output (once app development starts — see ARCHITECTURE.md §14)
# IDE (Eclipse/VS Code Java) output: copies of the Kotlin sources under a module's bin/ — never project files
# (added 2026-10-15, ai-sessions/0094 — ai-sessions/0093 A93-HK-07)
android/**/bin/
build/
.gradle/
local.properties
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ order:
cross-validation pass (`WORKSTATION_PREPARATIONS.md`) shows agreement
between two models: model agreement raises confidence, it does not
constitute the human review this rule requires.
- **Note (2026-10-15, maintainer-approved in chat, `ai-sessions/0094` — `ai-sessions/0093` A93-SES-08, D5):** a draft ADR may stand in
`DECISIONS.md` only with the maintainer's consent in chat, headed "(DRAFT — proposed, not accepted)" and registered `proposed` in `id_registry.csv`; it
decides nothing until the maintainer accepts it (as ADR-060).

## 7. Device Discovery & MAC Randomization

Expand Down
6 changes: 6 additions & 0 deletions AI_SESSION_LOG_PROCEDURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ Before a RESULT file's `Status` is set to `complete`:
(`complete (closed 2026-10-03 by …)`).
4. **In an audit that uses sub-reviews,** each finding says whether the main session re-derived it or took it from a sub-review (the M/S marks of
`ai-sessions/0068`). A sub-review claim that was not re-derived is an open question with its check, not a fact.
5. **The INDEX row agrees with the RESULT** (added 2026-10-15, `ai-sessions/0094` — `ai-sessions/0093` A93-HK-05/A93-SES-07): its status cell starts with
the same value as the RESULT's `Status`, and the next session that touches the record also updates the cell — `scripts/lint_docs.py` checks both, and an
unfilled `| (this commit) |` row in any record older than the newest session.
6. **Records are checked, not written from memory.** A compliance line quotes the command and its output (e.g. the merged manifest's permissions); a
count names the command that produced it; every document the prompt names as changed is re-read after the change. Stale "Next:" and "pull request
opened" lines in Summary and Progress are closed when the work they announce is done.

## 5. Multi-part results

Expand Down
6 changes: 4 additions & 2 deletions APK_REVERSE_ENGINEERING_PROCEDURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ the *what we found*, and `reverse-engineering/APK_VERSIONS.md` is the *which APK
looked at*.

This procedure follows `AGENTS.md` §6/`DECISIONS.md` ADR-017's AI-assistance boundary throughout:
an AI session may run the mechanical steps below (pulling, hashing, decompiling, running `pbtk`,
an AI session may run the mechanical steps below (pulling, hashing, decompiling, running the schema decoder — `pbtk` does not apply to this
APK, ADR-041 —
keyword/string searching, explaining already-surfaced code or disassembly — including native `.so`
disassembly output, per ADR-017 §4), but never decides which candidate is relevant to the protocol
and never decides that something becomes a recorded HYPOTHESIS in `REVERSE_ENGINEERING.md`. Both of
Expand Down Expand Up @@ -257,7 +258,8 @@ Account-Linking/Non-Owner traffic in captures) and move on — do not follow the

## 7. Checklist for a new APK version or a new Buds firmware (added 2026-10-03, `ai-sessions/0069`)

1. Pull and store the APK (§2); add its row to `reverse-engineering/APK_VERSIONS.md` with the tool versions.
1. Pull and store the APK (§2); add its row to `reverse-engineering/APK_VERSIONS.md` **the day it is pulled** (files and SHA-256 at once; the tool
versions when it is decompiled) — 1.0.990706425 was pulled on 2026-10-05 and registered only on 2026-10-15 (`ai-sessions/0093` A93-HK-03).
2. Decompile (§3). Re-run the schema decoder on the settings message (`qhr` in `v1.0.955078536`; the class name changes with every build —
find it by its field count and by the string literals of its callers, `REVERSE_ENGINEERING.md`).
3. Diff against the previous version (§2.1): the settings fields (numbers and types), the pw_rpc service and method name literals, the Fast Pair
Expand Down
82 changes: 51 additions & 31 deletions APP_TESTPLAN.md

Large diffs are not rendered by default.

43 changes: 25 additions & 18 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

130 changes: 58 additions & 72 deletions CAPTURE_BLUETOOTH_HCI_SNOOP.md

Large diffs are not rendered by default.

33 changes: 31 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@ build commit `bfd7891`, `ai-sessions/0089`; tested by the maintainer on the phon

## [Unreleased]

## [1.3.1]

Prepared in `ai-sessions/0094` (2026-10-15) from the audit `ai-sessions/0093` and the `CAP-076` findings (the maintainer's choice in chat 2026-10-15, "1.3.1 met
APP-01…06"). Nothing new on the wire: the app's requests are 1.3.0's. Not yet on hardware: the run is `CAP-077` (Group BM, planned).

### Fixed

- **"Use different Buds" now really forgets the Buds.** Before, the app could still connect to the same Buds by their name right afterwards; now it asks you to
pair (*Pair a device*) until you choose Buds in Android's picker. It also asks first, with **Cancel** and **Forget these Buds**.
- **After a reconnect, a mode that changed while the app was not connected keeps its line** "Changed since the app last saw it" (1.3.0's known issue, `CAP-076`).
- **A tap on the tile right after a Connect no longer does nothing without a word** (`CAP-076`): it changes the mode or says why not.
- A change the Buds push while the app is reading a setting no longer counts as the answer to that read; the read waits for its own answer.
- A settings file that cannot be read now gives the default settings instead of stopping the app's dark-mode and Debug-mode switches.

### Changed

- **One name for each noise-control mode everywhere**: "Noise cancellation", "Off", "Adaptive", "Transparency" — on the buttons, the (i), the Quick Settings tile
and the notification ("Connected — noise control: …"). The (i) says "Connection: connected" instead of a class name; the presets read "Heavy bass", not "HEAVY
BASS".
- **TalkBack** reads each switch with its name ("Earbuds replaced, switch, on"), each equalizer slider with its band and gain in dB, and each bottom tab once.

### Known issues

- As in 1.3.0: a tap on a noise-control mode claims the channel shared with Google Play services for about 1.5 seconds; on a phone with Play it may fail with a
message — tap again.
- As in 1.3.0: when the Buds close the session themselves, the mode shown can be a few seconds old until the re-open has read it again.
- Since 1.0.1: after you stop a ring by touching the bud, the app keeps saying it is ringing until you tap Stop.

## [1.3.0] - 2026-10-15

Prepared in `ai-sessions/0091` (2026-10-14) after `ai-sessions/0090`'s cross-check of the official app (`DECISIONS.md` ADR-064, ADR-065): the Buds report every
Expand Down Expand Up @@ -44,6 +72,7 @@ Connect, both byte-identical to the official app's.
moment (about 5 seconds after a claim) — the tap then usually fails with a message (in `CAP-076` once without one, from the tile: nothing changed); tap again.
- When the Buds close the session themselves (one bud out while both are worn — 12 of 77 such changes in the captures), the app re-opens it and reads the mode
again; in between, the mode shown can be a few seconds old.
*(Corrected 2026-10-15, `ai-sessions/0094`: 12 of **89** Buds-side changes in the official app's captures, not "12 of 77" — `PROTOCOL.md` §4.1, Correction of 2026-10-15.)*
- After a reconnect (or when the Buds re-open the session themselves), a mode that changed while the app was not connected shows as read: the (i) line
"Changed since the app last saw it" does not appear (`CAP-076`). A fix follows.

Expand Down Expand Up @@ -140,7 +169,7 @@ places, a held noise-control channel on its tab, a finer balance slider, and one
their (i) adds "These settings live on the case and are read when the app connects." The requests are unchanged.
- **Conversation detection** moved from Sound to **Controls** (its own card after In-ear detection, the same subtitle). Sound keeps Balance, Mono audio and Volume
EQ. The request is unchanged.
- The app sends thirteen requests at connect instead of twelve settings reads plus the EQ read and the subscription: the one `GetHardwareInfo` above.
- The app sends thirteen requests at connect instead of twelve settings reads plus the EQ read and the subscription: the one `GetHardwareInfo` above. *(Corrected 2026-10-15, `ai-sessions/0094`, A93-CAP-01: fifteen requests at connect — thirteen `ReadSetting`s (the EQ's `4:16` and twelve settings), `SubscribeRuntimeInfo` and the new `GetHardwareInfo`; `CAP-073`, 11 of 11 sessions.)*

### Studied, not built

Expand Down Expand Up @@ -173,7 +202,7 @@ AndroidX) so later work starts from a supported base. Prepared in `ai-sessions/0
9.3.3 with its built-in Kotlin, Kotlin 2.0.20 → 2.4.20, KSP → 2.3.12, Hilt 2.52 → 2.60.1, Compose BOM 2024.09.00 → 2026.09.00 (material3 1.4.0), AndroidX
and coroutines to their current stable releases, JUnit 5 → 6.1.3, Robolectric 4.17; Kotest removed (never used). Compiled against API 37; `minSdk` and
`targetSdk` stay 34 (`DECISIONS.md` ADR-029 Update). The app's permissions, components, texts and bytes on the wire are unchanged — same unit-test
counts (plus one new test for the settings-menu tab order), the same mutations caught; not yet hardware-tested (with the next release's run).
counts (plus one new test for the settings-menu tab order), the same mutations caught; not yet hardware-tested (with the next release's run). *(Hardware run `CAP-071`, `ai-sessions/0080`, 2026-10-08: passed — noted 2026-10-15, `ai-sessions/0094`, A93-HK-06.)*
- CI: `actions/checkout` v7.0.1 in all four workflows, `setup-python` v7.0.0, `setup-gradle` v6.4.0.

### Known issues
Expand Down
18 changes: 14 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Contributing

This project is currently a solo reverse-engineering effort (see `TODO.md` for
status), but is open source and PRs are welcome. Before contributing, read
`AGENTS.md`, `PROJECT_RULES.md`, and `PROJECT.md` — the same rules that bind
AI agents working on this project bind human contributors too.
This project is maintained by one person: an Android app with public releases (see `README.md` and
`CHANGELOG.md`) built on a reverse-engineered protocol (`PROTOCOL.md`). It is open source and PRs are
welcome. Before contributing, read `AGENTS.md`, `PROJECT_RULES.md`, and `PROJECT.md` — the same rules that
bind AI agents working on this project bind human contributors too.

## How a change gets in

`main` accepts changes only through a pull request (`RELEASING.md`, "Repository rules"). Fork, work on a
branch, and open a pull request against `main`. Two CI jobs must be green: `.github/workflows/android.yml`
(build, unit tests, lint, and the no-`INTERNET` check on the source and merged manifests) for a change under
`android/`, and `.github/workflows/lint-docs.yml` (`python3 scripts/lint_docs.py`) for a Markdown, registry or
lint-script change — run both locally first (`cd android && ./gradlew assembleDebug testDebugUnitTest test lint`;
`python3 scripts/lint_docs.py`). The maintainer reviews and merges. *(Section added 2026-10-15,
`ai-sessions/0094` — `ai-sessions/0093` A93-GOV-09.)*

## Code contributions

Expand Down
19 changes: 11 additions & 8 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ motivated this).
## ADR-002 — License: GNU AGPL-3.0

- **Date**: 2026-08-07
- **Status**: Accepted
- **Status**: Accepted *(provenance, 2026-10-15, `ai-sessions/0094`, maintainer-approved in chat 2026-10-15, `AskUserQuestion` "ADR-herkomst": decided by the maintainer at project start; no chat record exists — `ai-sessions/0093` A93-DEC-04)*
- **Context**: The project is open source and reconstructs a protocol through
reverse engineering; the license needs to protect that reverse-engineered
knowledge and any modified version of the app — including one deployed as a
Expand Down Expand Up @@ -129,7 +129,7 @@ motivated this).
## ADR-004 — No dependency on Google Play Services or any network permission

- **Date**: _(project start)_
- **Status**: Accepted
- **Status**: Accepted *(provenance, 2026-10-15, `ai-sessions/0094`, maintainer-approved in chat 2026-10-15, `AskUserQuestion` "ADR-herkomst": decided by the maintainer at project start; no chat record exists — `ai-sessions/0093` A93-DEC-04)*
- **Context**: The project's core motivation is an app that works fully
offline and independent of Google Play Services, including on GrapheneOS
where GMS may be absent or sandboxed.
Expand All @@ -147,7 +147,7 @@ motivated this).
## ADR-005 — Device discovery via CompanionDeviceManager, no continuous BLE scanning

- **Date**: _(project start)_
- **Status**: Accepted
- **Status**: Accepted *(provenance, 2026-10-15, `ai-sessions/0094`, maintainer-approved in chat 2026-10-15, `AskUserQuestion` "ADR-herkomst": decided by the maintainer at project start; no chat record exists — `ai-sessions/0093` A93-DEC-04)*
- **Context**: Continuous background BLE scanning is fingerprintable and
conflicts with GrapheneOS's threat model, which the app targets as its
primary reference OS.
Expand Down Expand Up @@ -222,7 +222,7 @@ motivated this).
## ADR-007 — `CAPTURE_BLUETOOTH_HCI_SNOOP.md` Groups are capture scenarios, not tests; `TESTPLAN_BLUETOOTH_HCI_SNOOP.md` is the test/behavior catalog

- **Date**: 2026-08-08
- **Status**: Accepted
- **Status**: Accepted *(provenance, 2026-10-15, `ai-sessions/0094`, maintainer-approved in chat 2026-10-15, `AskUserQuestion` "ADR-herkomst": decided by the maintainer at project start; no chat record exists — `ai-sessions/0093` A93-DEC-04)*
- **Context**: `CAPTURE_BLUETOOTH_HCI_SNOOP.md`'s Groups A–Q and
`TESTPLAN_BLUETOOTH_HCI_SNOOP.md`'s four action tables had grown to
describe largely the same ~66–70 Buds actions/behaviors, but with different
Expand Down Expand Up @@ -2697,7 +2697,7 @@ motivated this).
## ADR-061 — Hold the Message Stream claim while the noise-control tab is on screen

- **Date**: 2026-10-10
- **Status**: Accepted (maintainer, chat 2026-10-10, `ai-sessions/0083`) — built in the 1.2.0 rebuild (`ai-sessions/0084`). **Superseded by ADR-065 (2026-10-14,
- **Status**: Superseded by ADR-065 (2026-10-14) — was: Accepted (maintainer, chat 2026-10-10, `ai-sessions/0083`) — built in the 1.2.0 rebuild (`ai-sessions/0084`). **Superseded by ADR-065 (2026-10-14,
`ai-sessions/0091`): the hold is removed in 1.3.0 — the mode comes from the Buds' settings stream on the app's own channel.** The text below is kept as written.
- **Note on process**: drafted by an AI agent (`ai-sessions/0083`); the decision is the maintainer's, given in the chat of 2026-10-10 (`AskUserQuestion` "Hold
result", option *"Fix first: hold 0x04 on ANC tab"*, and "Hold ADR", option *"Accept as ADR-061 now (Recommended)"*, with this text in the preview), per
Expand Down Expand Up @@ -2733,7 +2733,7 @@ motivated this).
## ADR-062 — A user-selectable re-claim of the noise-control channel after a foreign close, with a back-off

- **Date**: 2026-10-12
- **Status**: Accepted (maintainer, chat 2026-10-12, `ai-sessions/0087`) — supersedes ADR-061's clause "the Buds (or another client) closing DLCI 0x04 ends it
- **Status**: Superseded by ADR-065 (2026-10-14) — was: Accepted (maintainer, chat 2026-10-12, `ai-sessions/0087`) — supersedes ADR-061's clause "the Buds (or another client) closing DLCI 0x04 ends it
without a re-claim" for the switch's mode "take it back" only; ADR-061's hold and ADR-032's claim model stand. Built in 1.2.1 (`ai-sessions/0087`);
hardware-verified by `CAP-075` (its Update of 2026-10-13). **Superseded by ADR-065 (2026-10-14, `ai-sessions/0091`), with its Updates: the re-claim, the
setting, its card and "Take it back again" are removed in 1.3.0; a stored `hold_reclaim_mode` is ignored.** The text below is kept as written.
Expand Down Expand Up @@ -2809,7 +2809,7 @@ motivated this).
## ADR-063 — The Settings card may say whether Google Play services is installed on this phone (information only)

- **Date**: 2026-10-13
- **Status**: Accepted (maintainer, chat 2026-10-13, `ai-sessions/0089`) — built in 1.2.2; never hardware-verified (1.2.2 was released without `CAP-076`).
- **Status**: Superseded by ADR-065 (2026-10-14) — was: Accepted (maintainer, chat 2026-10-13, `ai-sessions/0089`) — built in 1.2.2; never hardware-verified (1.2.2 was released without `CAP-076`).
**Superseded by ADR-065 (2026-10-14, `ai-sessions/0091`): the Play-services line, `PlayServicesProbe` and the `<queries>` entry are removed in 1.3.0.** The
text below is kept as written.
- **Note on process**: drafted by an AI agent (`ai-sessions/0089`); the information line is the maintainer's choice in chat 2026-10-13 (`AskUserQuestion` "Play
Expand Down Expand Up @@ -2838,7 +2838,7 @@ motivated this).
## ADR-064 — DLCI 0x02: `SubscribeToSettingsChanges` and `ReadSetting 4:13` — the noise-control mode on the app's own channel (read only)

- **Date**: 2026-10-14
- **Status**: Accepted (maintainer, chat 2026-10-14, `ai-sessions/0090`) — not yet built; its hardware run comes with the build
- **Status**: Accepted (maintainer, chat 2026-10-14, `ai-sessions/0090`) — not yet built; its hardware run comes with the build *(Status pointer, 2026-10-15, `ai-sessions/0094`, maintainer-approved in chat 2026-10-15, `AskUserQuestion` "ADR-teksten", A93-DEC-01: built in 1.3.0 (`ai-sessions/0091`), hardware run `CAP-076` (`ai-sessions/0092`), released 2026-10-15; items 2 and 6 amended by ADR-065)*
- **Note on process**: drafted by an AI agent (`ai-sessions/0090`); the decision is the maintainer's, given in the chat of 2026-10-14 (`AskUserQuestion`
"ADR-064", option *"Accept now, build next (Recommended)"*, with this text in the preview), per `AGENTS.md` §6. It rests on the `PROTOCOL.md` §4.1 Update of
2026-10-14 (setting 13 = the mode, 🟢), approved in the same chat.
Expand All @@ -2851,6 +2851,9 @@ motivated this).
DLCI 0x02 is the app's own session channel, never contended by another client; the Buds close it themselves on some wear changes (L-1, ADR-044).
- **Options considered**: (a) keep the Message Stream as the only source (ADR-061/062); (b) this decision, read only; (c) (b) plus switching ANC with
`WriteSetting 4:{13:n}` — not now: no capture holds such a write (0 in 96 logs).
- **Note (2026-10-15, `ai-sessions/0094`, maintainer-approved in chat 2026-10-15, `AskUserQuestion` "ADR-teksten", `ai-sessions/0093` A93-DEC-02):** the Context's "73 of 73 Buds-side … changes" counts the class defined as "pushed"; re-derived:
75 of 89 Buds-side changes pushed within 1 s, 12 of 89 lost to the Buds' `DISC` of MAESTRO, 2 at re-opens (`PROTOCOL.md` §4.1, Correction of 2026-10-15). The decision
is unchanged — ADR-065 covers the `DISC` case with the re-open's `ReadSetting 4:13`.
- **Decision**:
1. Once per MAESTRO session (Connect and every ADR-044 re-open), after `SubscribeRuntimeInfo`: one `SubscribeToSettingsChanges` REQUEST (empty payload,
byte-identical to `CAP-051` 1177 on channel 21 — `7e 00 4b 03 10 15 1d ea 71 de 7d 5e 25 f5 ad 21 28 01 97 54 b0 7e` — or `CAP-056` 1451 on channel 19)
Expand Down
Loading
Loading