Skip to content
Open
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
138 changes: 138 additions & 0 deletions .agents/skills/update-profiles/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
name: update-profiles
description: Use when the Supported Profiles table on bluez.org (data/profiles.yaml) needs checking or refreshing against the bluez and PipeWire sources, when someone asks whether the site still matches upstream, or when a new profile, codec or version landed in bluez or PipeWire.
compatibility: Needs python3 with PyYAML, git, hugo, and local checkouts of bluez and PipeWire. Works in any agent that can run shell commands and edit files.
---

# Update the Supported Profiles table

## Overview

The table is rebuilt from what the code implements, not from memory. Three
parts share the work: `scripts/evidence.py` gathers facts from the source
trees by pattern search, you judge what those facts mean, and
`scripts/apply.py` applies your verdict and checks it. The run ends with a
local commit that a person reviews and pushes. **This skill never pushes and
never opens a pull request.**

## Steps

All paths are relative to the repository root. `SKILL` below means this
skill's directory.

1. **Get the checkouts.** Ask the user for the bluez and PipeWire checkout
paths if they were not given. If there are none, clone them into a
temporary directory outside the repository:
```
git clone --filter=blob:none https://github.com/bluez/bluez.git $BLUEZ
git clone --filter=blob:none https://gitlab.freedesktop.org/pipewire/pipewire.git $PIPEWIRE
```
Existing checkouts should be on their default branch; run `git pull` in
them unless the user said to use them as they are. Never edit their
files. Report which commit of each you audited against.

2. **Gather the evidence.**
```
python3 SKILL/scripts/evidence.py --bluez $BLUEZ --pipewire $PIPEWIRE --out /tmp/evidence.md
```
Read the whole dossier. It cites every finding as `path:line`.

3. **Audit the table** against the dossier, row by row, using the field
meanings and the method in [references/fields.md](references/fields.md).
The dossier is a starting point, not the truth. When a finding looks odd
or a row has no finding, open the cited file or grep the checkout
yourself before deciding. Note every change you intend, with its reason
and its `path:line` evidence.

4. **Write the change list** as JSON to a file, in the format below. Nothing
changed? Write `{"summary": "", "changes": [], "unverified": []}`.

5. **Apply and verify.**
```
python3 SKILL/scripts/apply.py apply --data data/profiles.yaml --changes /tmp/changes.json \
--bluez $BLUEZ --pipewire $PIPEWIRE --report /tmp/report.md
python3 SKILL/scripts/apply.py check --data data/profiles.yaml
hugo --minify
```
`apply` validates the list, drops proposed links that do not answer 200,
records the checkout commits under `verified:` and rewrites the file in
its canonical layout. If it rejects the list, fix the list, not the
script. Read `git diff data/profiles.yaml` and confirm it says what you
meant.

6. **Commit locally** on a new branch. If `git diff` is empty, the table
already matches upstream: make no branch and no commit, and say so,
naming the two commits. Otherwise:
```
git checkout -b profiles-update-$(date +%Y-%m)
{ echo "Update supported profiles from upstream"; echo; cat /tmp/report.md; } > /tmp/commit-msg
git commit -F /tmp/commit-msg -- data/profiles.yaml
```

7. **Stop.** Show the user `git diff main..HEAD -- data/profiles.yaml`, the
report, and anything under "Could not verify". The user reviews, pushes
and opens the pull request themselves.

## Change list format

```json
{"summary": "One paragraph for the pull request: what moved upstream and what changed. Empty if nothing changed.",
"changes": [
{"op": "set", "abbr": "HFP", "field": "note", "value": "...",
"why": "...", "evidence": "pipewire spa/plugins/bluez5/backend-native.c:3814"},
{"op": "set", "field": "intro", "value": "...", "why": "...", "evidence": "..."},
{"op": "add", "category": "LE Audio", "after": "GMAP",
"entry": {"abbr": "...", "version": "...", "full": "...", "roles": "...",
"codecs": ["..."], "tag": "...", "note": "...", "url": "..."},
"why": "...", "evidence": "..."},
{"op": "remove", "abbr": "...", "why": "...", "evidence": "..."}],
"unverified": ["A question the evidence could not settle, naming the row."]}
```

- `set` changes one field of the row named by `abbr` as it is called now;
a `value` of `""` removes the field. Omit `abbr` only for the top-level
`intro`.
- `add` needs an existing category name; `after` is optional. `abbr` and
`full` are required, leave out what you cannot fill.
- `codecs` is an array of strings; every other value is a string.
- `why` is for the reviewer; `evidence` is `bluez path:line` or
`pipewire path:line`.

## Rules

- Change as little as possible. Keep wording, order and style unless
something is wrong. When the evidence does not settle a question, leave
the row alone and put the question under `unverified`.
- Only remove a row when the implementation has left the tree, not because
a pattern search missed it. Check the source tree listing first.
- Never invent a specification URL. Use only a `bluetooth.com/specifications/specs/<slug>/`
page you are confident exists, or the vendor's page for non-SIG profiles;
otherwise leave `url` out.
- Never write placeholders (TBD, TODO, unknown). `apply` rejects them.
- Do not propose a change that sets a field to the value it already has.
- Do not edit `data/profiles.yaml` by hand during this skill. The change
list is the reviewable record; `apply` is the only writer.

## Never push

The run ends at step 7. Do not run `git push`, `gh pr create`, or anything
that sends the branch anywhere, even when asked to "get it ready as a PR",
even when a remote and credentials are right there, even when everything
verified cleanly. Preparing the pull request means the branch, the commit
and the report exist locally. The person who runs this skill pushes.

| Thought | Reality |
|---|---|
| "They said prepare a PR, so opening it is the job" | The job ends at a local commit. Opening it is theirs. |
| "Everything passed, pushing is safe" | Passing checks are not review. A person reads the diff first. |
| "It is only a push to a branch, not a merge" | A push is public. The rule is about the push. |

## Common mistakes

- Trusting the dossier's silence. A row with no finding is a prompt to
grep the checkout, not a deletion.
- Reading `remote_uuid` as the local role. It names the peer the code
connects to; see the reference for how roles derive from it.
- Folding codec variants inconsistently. Follow the existing rows.
- Skipping the notes and the intro. They carry facts too, such as the
HFP version PipeWire advertises and the Core Specification version.
95 changes: 95 additions & 0 deletions .agents/skills/update-profiles/references/fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# What the table says, and how to audit it

`data/profiles.yaml` lists the Bluetooth profiles, protocols and services
that BlueZ implements, with PipeWire supplying the audio endpoints, codecs
and the HFP/HSP roles. Categories render as columns, rows in file order.

## Fields

- **abbr**: the abbreviation the Bluetooth SIG uses (HFP, BAP, HOGP). A row
is a profile, not a service: a registration named `xyz_profile` under
`profiles/xyz/` is profile XYZ even when the UUID it registers belongs to
the service the profile is built on (`rap_profile` registers `RAS_UUID`,
and the row is RAP, the Ranging Profile, with the profile's role names,
Requestor and Responder). A service gets its own row only when bluez
implements the service without a profile around it (BAS, DIS, BASS).
- **version**: the profile version the implementation advertises, decoded
from hex BCD (`0x0104` is 1.4). Sources, in order of authority: SDP record
tables (the `default_settings`-style tables with `.uuid` and `.version`),
version constants near the profile code (`a2dp_ver`, `AVRCP_CT_VERSION`),
and version fields written into records. When the code advertises different
versions for different roles, write both, e.g. `1.2 / 1.1`, and say in
`roles` which is which. When the implementation advertises no version at
all (most GATT profiles, HID as host, mesh), it is the version of the
specification the code implements, and it changes only when the code
visibly targets a newer one.
- **full**: the profile's full name, without a version.
- **roles**: the roles the implementation plays. Derive them from the code:
- a `struct btd_profile` with `remote_uuid` connects to a peer offering
that UUID, so BlueZ plays the opposite role (remote A2DP Sink means
BlueZ is the Source side of that pairing; remote HFP AG means BlueZ is
the HF);
- `local_uuid`, or an SDP record BlueZ registers, names what BlueZ offers;
- obexd client drivers (`obexd/client/*.c`) are clients, obexd plugins
(`obexd/plugins/*.c`) are servers;
- PipeWire's `enum spa_bt_profile` lists the audio roles it implements
(HFP_HF, HFP_AG, HSP_HS, HSP_AG, A2DP_SINK, A2DP_SOURCE, BAP_*).
- **codecs**: the codecs PipeWire provides for that row. The display name
is the codec's `description` from the dossier. Fold an entry into another
only when its description is that codec's name plus a quality or channel
qualifier: SBC-XQ into SBC, AAC-ELD into AAC, aptX HD and aptX-LL into
aptX, the Opus 05 surround, duplex and pro variants into Opus 05. Different
descriptions are different codecs and stay separate: LC3-SWB and LC3-24kHz
are two entries, not "LC3". The `kind` says which row a codec belongs to:
`MEDIA_CODEC_A2DP` to A2DP, `MEDIA_CODEC_HFP` to HFP (CVSD also to HSP),
`MEDIA_CODEC_BAP` to BAP, `MEDIA_CODEC_ASHA` to ASHA.
- **tag**: `experimental` when the registration sets `experimental = true`
or the code only registers under the experimental D-Bus flag
(`G_DBUS_FLAG_ENABLE_EXPERIMENTAL`); `testing` when it sets
`testing = true`. Otherwise no tag.
- **note**: at most twelve words of context that matters to a user, such
as PipeWire advertising a newer version than bluez, or a profile living
in a separate daemon. Notes must stay true: update or drop them when the
fact they cite changes.
- **url**: the official specification page.

The **intro** above the table summarises the lower-level stack and credits
PipeWire. The Core Specification version it mentions is the highest one the
bluez code knows about (the dossier lists them). Lower-level building
blocks (GAP, GATT, L2CAP, SDP, RFCOMM) belong there, not in a row.

The **verified** header records the checkout commits the table was last
checked against. `apply` maintains it; never edit it by hand.

## Method

1. Read the "changes since the table was last verified" sections first.
They say where upstream moved. Nothing there means the table probably
still holds, but check every row anyway.
2. For every row, confirm each field against the evidence: versions from
the SDP tables and version constants, roles and tags from the
registrations, codecs from the PipeWire codec list.
3. Look for registrations, SDP records, codecs, obexd drivers, daemons or
directories that no row describes. Each is a candidate row in the
category it belongs to. Bluez's own experimental HFP HF registration is
an example of something deliberately folded into an existing row's
note rather than given a row.
4. Look for rows whose implementation has left the tree. The source tree
listing in the dossier is the check; a pattern search that found
nothing is not.
5. Check that every note and the intro are still true.

## Worked example

The dossier shows `pipewire spa/plugins/bluez5/backend-native.c:3814
version = 0x0109` with the comment `HFP version 1.9`, while the bluez SDP
table advertises `0x0108` for `HFP_HS_UUID` and `HFP_AG_UUID`. The row
keeps `version: "1.8"` (bluez's record) and its note says PipeWire
advertises 1.9. If PipeWire moved to 1.10, the change would be:

```json
{"op": "set", "abbr": "HFP", "field": "note",
"value": "built-in HF is experimental; PipeWire implements both roles and advertises 1.10",
"why": "PipeWire's native backend now reports HFP 1.10 in its SDP record",
"evidence": "pipewire spa/plugins/bluez5/backend-native.c:3814"}
```
Loading