# Watch Multi-Profile Build Mechanism Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Enable safe parallel testing of the Wear OS app across three named build profiles (`brummel-dev`, `brummel-minerva`, `krey-minerva`) with a hard-locked watch-serial whitelist, so Brummel can switch between dev and minerva on his watch and Dr. Krey can use her own watch against minerva without risk of cross-contamination.

**Architecture:** Each profile is one bash-source-bare file under `clients/wearos/profiles.d/<name>.sh` (gitignored) holding `SERVER_URL`, `API_KEY`, and `ALLOWED_SERIALS[]`. `run.sh` takes the profile as a required second positional after `watch`, sources the file, refuses to proceed if the attached watch's ADB serial is not in `ALLOWED_SERIALS`, then injects the values into Gradle as `-Pdoctate.serverUrl=`, `-Pdoctate.apiKey=`, and `-Pdoctate.profileName=`. Build-time `BuildConfig.PROFILE_NAME` + `IS_DEV_PROFILE` plus a manifest-placeholder app label make the active profile visible on the watch.

**Tech Stack:** Bash (zero new deps), Gradle/Kotlin DSL, Kotlin/Compose for the optional dev badge, Android manifest placeholders.

**Spec:** [docs/specs/2026-05-20-watch-multi-profile-design.md](../specs/2026-05-20-watch-multi-profile-design.md)

---

## File Structure

**New files:**
- `clients/wearos/profiles.d/` — directory holding one bash file per profile
- `clients/wearos/profiles.d/example-profile.sh.example` — checked-in template
- `clients/wearos/profiles.d/brummel-dev.sh` — gitignored (created in Task 1)
- `clients/wearos/profiles.d/brummel-minerva.sh` — gitignored (created in Task 7)
- `clients/wearos/profiles.d/krey-minerva.sh` — gitignored (created in Task 7)
- `clients/wearos/app/src/main/java/com/doctate/watch/presentation/components/DevBadge.kt` — optional, Task 10

**Modified files:**
- `clients/wearos/.gitignore` — add `profiles.d/*.sh` rule plus example exception
- `clients/wearos/run.sh` — new helpers, argument parser extension, deprecated env-vars removed (Tasks 2–4)
- `clients/wearos/local.properties` — remove `doctate.apiKey` line (Task 5)
- `clients/wearos/app/build.gradle.kts` — `PROFILE_NAME` + `IS_DEV_PROFILE` BuildConfig fields, `appLabel` manifest placeholder (Task 8)
- `clients/wearos/app/src/main/AndroidManifest.xml` — `android:label="${appLabel}"` (Task 9)
- `clients/wearos/app/src/main/java/com/doctate/watch/presentation/CaseListScreen.kt` — mount `DevBadge` (optional, Task 10)
- `clients/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingScreen.kt` — mount `DevBadge` (optional, Task 10)

---

## Prerequisites (external, manual — not code edits)

These three pre-requisite tasks happen outside this repo. They can run in parallel with each other; **Prereq 1 and Prereq 2 must complete before the smoke-test in Task 11 can succeed** for any minerva profile.

### Prereq 1: Resolve Issue #1 — minerva users.toml plaintext keys

- [ ] SSH to minerva, open `/opt/stacks/doctate-server/config/users.toml`
- [ ] For each existing `[[user]]` block whose `api_key` field starts with `$2a$`, `$2b$`, or `$2y$`: replace the bcrypt-shaped value with a fresh plaintext value generated by `openssl rand -base64 24`. Keep one entry per user (currently: `brummel`).
- [ ] Record each new plaintext value — you will paste it into the corresponding profile file in Task 1 / Task 7. **These plaintext values never go into the repo.**
- [ ] Reload the doctate container (Dockge UI on `minerva.lan:5001` or the equivalent `docker compose` command in the deployment skill)
- [ ] Verify with `curl -i -H "X-API-Key: <new-plaintext>" http://minerva.lan:3000/api/oneliners` → expect HTTP 200

### Prereq 2: Add Krey user on minerva

- [ ] In the same `/opt/stacks/doctate-server/config/users.toml` on minerva, add a new `[[user]]` block:

```toml
[[user]]
slug = "krey"
display_name = "Dr. Krey"
api_key = "<plaintext from openssl rand -base64 24>"
web_password = "<bcrypt hash — fill in via Prereq 2b>"
role = "doctor"
```

- [ ] Prereq 2b: Generate the bcrypt hash for `web_password` by running the `hash-password` CLI from the server container (or locally) — `cargo run --bin hash-password -- --user krey` if running locally, otherwise the equivalent `docker exec` invocation. Paste the resulting hash into the `web_password` field of the `krey` block, replacing the placeholder.
- [ ] Reload the doctate container
- [ ] Verify by logging into `http://minerva.lan:3000/` in a browser as `krey` with the chosen password → expect to land on the case list with `role=doctor` UI (no Reset/Bulk/Purge buttons visible)
- [ ] Record Krey's plaintext `api_key` for Task 7

### Prereq 3: Collect ADB serials of both watches

- [ ] For Brummel's watch: `cd clients/wearos && ./run.sh connect <watch-host:port>` (use the IP from the watch's ADB Debug Bridge screen). After connect, run `./run.sh devices` and copy the line that looks like `adb-XXXXXXXX-yyyy._adb-tls-connect._tcp` — this is the serial.
- [ ] Repeat for Krey's watch when physically available, recording the second serial.
- [ ] Save both serials in your notes (or `/tmp/`) — you will paste them into profile files in Task 1 / Task 7.

---

## Implementation Tasks

### Task 1: Create profiles.d/ skeleton (template, .gitignore, first profile)

This task adds the directory structure, the gitignore rule, the checked-in `*.sh.example` template, and the first real profile (`brummel-dev`) so the run.sh changes in Task 2/3 have something to load.

**Files:**
- Create: `clients/wearos/profiles.d/example-profile.sh.example`
- Create: `clients/wearos/profiles.d/brummel-dev.sh` (gitignored, local only)
- Modify: `clients/wearos/.gitignore`

- [ ] **Step 1: Create the profiles.d/ directory**

```bash
mkdir -p clients/wearos/profiles.d
```

- [ ] **Step 2: Write the checked-in template**

Create `clients/wearos/profiles.d/example-profile.sh.example` with this exact content:

```bash
# Watch build profile template. Copy this file to <profile-name>.sh in the
# same directory (the .sh files are gitignored). The profile name (the file
# basename without .sh) is what you pass to ./run.sh, e.g.:
#
#     ./run.sh watch brummel-dev install
#
# Naming convention:
#   <user>-<server>     e.g. brummel-dev, brummel-minerva, krey-minerva
#   Profiles whose name ends in "-dev" are treated as development builds
#   (launcher label "Doctate (Dev)" + red "DEV" badge in the UI). All other
#   profiles render the production-clean UI.

# Backend URL the watch talks to. Plain HTTP is fine on the LAN.
SERVER_URL="http://minerva.lan:3000"

# Plaintext api_key matching the slug in the server's users.toml.
# Generate with: openssl rand -base64 24
API_KEY="REPLACE_ME_WITH_PLAINTEXT_API_KEY"

# Bash array of ADB serials (one per attached watch this profile may target).
# Find a watch's serial with: ./run.sh devices
# Brummel's watch can appear in multiple profiles; Krey's watch only in hers.
ALLOWED_SERIALS=(
    "adb-XXXXXXXX-yyyy._adb-tls-connect._tcp"
)
```

- [ ] **Step 3: Add gitignore rules**

Append to `clients/wearos/.gitignore` (existing content is preserved):

```
profiles.d/*.sh
!profiles.d/*.sh.example
```

Verify with `git check-ignore -v clients/wearos/profiles.d/brummel-dev.sh` (after the file exists in Step 4) — expect a match line pointing to `profiles.d/*.sh`.

- [ ] **Step 4: Create the brummel-dev profile**

Create `clients/wearos/profiles.d/brummel-dev.sh` filled with real values (substituting `<...>` placeholders below with values from Prereq 3 and the dev server's plaintext key):

```bash
SERVER_URL="http://192.168.178.27:3000"
API_KEY="<plaintext from server's users.toml (laptop) under slug 'brummel'>"
ALLOWED_SERIALS=(
    "<Brummel watch serial from Prereq 3>"
)
```

If the laptop's `server/users.toml` does not yet have a plaintext `api_key` for `brummel`, set one now: replace the current value with `openssl rand -base64 24` output and use the same value here.

- [ ] **Step 5: Verify gitignore handles it correctly**

```bash
git status clients/wearos/profiles.d/
```

Expected output (no `.sh` listed as untracked, only the `.sh.example`):

```
?? clients/wearos/profiles.d/example-profile.sh.example
```

- [ ] **Step 6: Commit the template + gitignore rule (NOT the local profile)**

```bash
git add clients/wearos/profiles.d/example-profile.sh.example clients/wearos/.gitignore
git commit -m "feat(wear): add profiles.d/ scaffolding for multi-profile builds

Adds the directory, gitignore rule, and checked-in template. Real profile
files (e.g. brummel-dev.sh) live alongside but are gitignored as they
contain plaintext api_keys.

refs #3"
```

---

### Task 2: Add load_profile + verify_serial_allowed helpers to run.sh

The two helpers are pure functions that operate on the profile filename and the resolved ADB serial. They die loudly on any inconsistency. They do not yet wire into the dispatch path — that comes in Task 3.

**Files:**
- Modify: `clients/wearos/run.sh` (insert helpers between the "Target resolution" section ending around line 240 and `apply_target` starting around line 280)

- [ ] **Step 1: Insert load_profile and verify_serial_allowed before apply_target**

Find this block in `clients/wearos/run.sh` (search for `apply_target() {`, somewhere around line 280):

```bash
apply_target() {
    case "$DOCTATE_TARGET" in
        watch)
```

Immediately ABOVE the `apply_target() {` line, insert:

```bash
# --- Profile loading (watch target only) -----------------------------------
# Profiles live in profiles.d/<name>.sh and define SERVER_URL, API_KEY, and
# ALLOWED_SERIALS as a bash array. The .sh files are gitignored — see
# profiles.d/example-profile.sh.example for the format.

PROFILE_NAME=""
SERVER_URL=""
API_KEY=""
ALLOWED_SERIALS=()

load_profile() {
    local name="$1"
    local file="$SCRIPT_DIR/profiles.d/$name.sh"
    if [[ ! -f "$file" ]]; then
        local available
        available="$(cd "$SCRIPT_DIR/profiles.d" 2>/dev/null && \
                     ls *.sh 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
        [[ -z "$available" ]] && available="<none — create one in profiles.d/ from the .sh.example template>"
        die "Unknown profile '$name'. Available: ${available}"
    fi
    # Reset to detect missing fields in the profile file.
    SERVER_URL=""; API_KEY=""; ALLOWED_SERIALS=()
    # shellcheck disable=SC1090
    source "$file"
    [[ -n "$SERVER_URL" ]]              || die "Profile '$name': SERVER_URL missing"
    [[ -n "$API_KEY"    ]]              || die "Profile '$name': API_KEY missing"
    [[ "${#ALLOWED_SERIALS[@]}" -gt 0 ]] || die "Profile '$name': ALLOWED_SERIALS empty"
    PROFILE_NAME="$name"
}

# Aborts (via die) if $1 is not present in ALLOWED_SERIALS. The error message
# names BOTH the attached serial and the allow-list, so the user can see at a
# glance whether they grabbed the wrong watch or typed the wrong profile.
verify_serial_allowed() {
    local serial="$1"
    local s
    for s in "${ALLOWED_SERIALS[@]}"; do
        [[ "$s" == "$serial" ]] && return 0
    done
    die "Watch serial '$serial' is NOT in ALLOWED_SERIALS for profile '$PROFILE_NAME'.
Allowed: ${ALLOWED_SERIALS[*]}
This refusal protects against installing the wrong key on the wrong watch
(e.g. Brummel-key on Krey's watch → patient data goes to dev server)."
}

```

- [ ] **Step 2: Smoke-verify load_profile against the brummel-dev file**

The function does not yet run as part of the main dispatch. To verify in isolation, run from `clients/wearos/`:

```bash
bash -c 'source ./run.sh; ' 2>&1 | head -5
```

This will error out because `run.sh` runs `apply_target` at the bottom even when sourced. Instead, use this isolated probe (paste at the shell, replace `<expected-url>` with the URL you put into `brummel-dev.sh`):

```bash
bash -ec '
    SCRIPT_DIR="'"$PWD"'"
    die() { echo "ERR: $*" >&2; exit 1; }
    PROFILE_NAME=""; SERVER_URL=""; API_KEY=""; ALLOWED_SERIALS=()
    load_profile() {
        local name="$1"
        local file="$SCRIPT_DIR/profiles.d/$name.sh"
        [[ -f "$file" ]] || die "no such profile: $name"
        SERVER_URL=""; API_KEY=""; ALLOWED_SERIALS=()
        source "$file"
        [[ -n "$SERVER_URL" ]] || die "SERVER_URL missing"
        [[ -n "$API_KEY"    ]] || die "API_KEY missing"
        [[ "${#ALLOWED_SERIALS[@]}" -gt 0 ]] || die "ALLOWED_SERIALS empty"
        PROFILE_NAME="$name"
    }
    load_profile brummel-dev
    echo "OK profile=$PROFILE_NAME url=$SERVER_URL serials=${ALLOWED_SERIALS[*]}"
'
```

Expected: `OK profile=brummel-dev url=http://192.168.178.27:3000 serials=adb-...`

- [ ] **Step 3: Smoke-verify the unknown-profile error path**

Reuse the probe above but call `load_profile no-such-profile`. Expected: `ERR: no such profile: no-such-profile`, exit code non-zero.

- [ ] **Step 4: Commit the helper functions**

```bash
git add clients/wearos/run.sh
git commit -m "feat(wear): add load_profile + verify_serial_allowed helpers

Helpers source profiles.d/<name>.sh and assert SERVER_URL, API_KEY, and
ALLOWED_SERIALS. verify_serial_allowed dies with an explicit cross-watch
warning when the attached serial is not in the profile's allow-list. Not
yet wired into apply_target — that follows in the next commit.

refs #3"
```

---

### Task 3: Rewire apply_target for watch + extend argument parser

The dispatcher at the bottom of `run.sh` learns to consume the second positional argument as the profile name when target is `watch`. `apply_target` calls `load_profile` and `verify_serial_allowed` before any Gradle invocation.

**Files:**
- Modify: `clients/wearos/run.sh` (dispatch block around line 530, `apply_target` body around line 280)

- [ ] **Step 1: Replace the watch branch of apply_target**

Find this block in `apply_target`:

```bash
        watch)
            if [[ -z "$ADB_SERIAL" ]]; then
                resolve_watch_serial
                ADB_SERIAL="$RESOLVED_SERIAL"
            fi
            local server="${DOCTATE_DEV_SERVER:-$DEFAULT_DEV_SERVER}"
            GRADLE_PROPS+=("-Pdoctate.serverUrl=$server")
            info "Target: watch (serial=$ADB_SERIAL, server=$server)"
            ;;
```

Replace it with:

```bash
        watch)
            [[ -n "$DOCTATE_PROFILE" ]] || die \
                "Watch target requires a profile. Usage: ./run.sh watch <profile> <subcmd>
Available profiles: $(cd "$SCRIPT_DIR/profiles.d" 2>/dev/null && ls *.sh 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
            load_profile "$DOCTATE_PROFILE"
            if [[ -z "$ADB_SERIAL" ]]; then
                resolve_watch_serial
                ADB_SERIAL="$RESOLVED_SERIAL"
            fi
            verify_serial_allowed "$ADB_SERIAL"
            GRADLE_PROPS+=(
                "-Pdoctate.serverUrl=$SERVER_URL"
                "-Pdoctate.apiKey=$API_KEY"
                "-Pdoctate.profileName=$DOCTATE_PROFILE"
            )
            info "Target: watch (profile=$DOCTATE_PROFILE, serial=$ADB_SERIAL, server=$SERVER_URL)"
            ;;
```

- [ ] **Step 2: Drop the deprecated DOCTATE_API_KEY pass-through**

Find this block, also in `apply_target` (a few lines below the watch case, after the emulator/'' cases):

```bash
    if [[ -n "${DOCTATE_API_KEY:-}" ]]; then
        GRADLE_PROPS+=("-Pdoctate.apiKey=$DOCTATE_API_KEY")
    fi
```

Delete it entirely. (The api_key is now sourced from the profile, never from an env var.)

- [ ] **Step 3: Add DOCTATE_PROFILE declaration to the config section**

Find the line near the top of run.sh:

```bash
DOCTATE_TARGET="${DOCTATE_TARGET:-}"
```

Add a new line immediately below it:

```bash
DOCTATE_PROFILE="${DOCTATE_PROFILE:-}"
```

- [ ] **Step 4: Extend the dispatcher to consume the profile positional**

Find this block around line 530 (search for `if [[ "${1:-}" == "watch"`):

```bash
if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then
    DOCTATE_TARGET="$1"
    shift
fi
```

Replace it with:

```bash
if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then
    DOCTATE_TARGET="$1"
    shift
    # The watch target requires a profile name as the next positional. The
    # emulator target does not — it always uses the hardcoded loopback URL.
    if [[ "$DOCTATE_TARGET" == "watch" ]]; then
        # Target-free subcommands that follow `watch` directly (none today,
        # but kept defensively) should not be misread as a profile name.
        case "${1:-}" in
            ""|help|-h|--help|devices|connect|clean)
                # No profile, fall through — apply_target will die with a
                # clear usage message if the subcommand actually needs one.
                ;;
            *)
                DOCTATE_PROFILE="$1"
                shift
                ;;
        esac
    fi
fi
```

- [ ] **Step 5: Smoke-verify the new dispatch — happy path**

From `clients/wearos/`, run a build-only command (which exercises argument parsing + `load_profile` + `verify_serial_allowed` but does not require a connected watch):

```bash
./run.sh watch brummel-dev help
```

Expected: the help text prints normally (since `help` is a target-free subcommand that short-circuits before `apply_target`). This proves the profile positional doesn't break the short-circuit path.

- [ ] **Step 6: Smoke-verify the missing-profile error**

```bash
./run.sh watch install 2>&1 | head -5
```

Expected: error message starting with `ERR Watch target requires a profile. Usage:...` and the list of available profiles. Exit code non-zero.

- [ ] **Step 7: Smoke-verify the unknown-profile error**

```bash
./run.sh watch bogus-profile install 2>&1 | head -3
```

Expected: error message starting with `ERR Unknown profile 'bogus-profile'. Available: brummel-dev`. Exit code non-zero. (`apply_target` is reached but `load_profile` dies before Gradle.)

- [ ] **Step 8: Commit the dispatch changes**

```bash
git add clients/wearos/run.sh
git commit -m "feat(wear): wire watch profile into apply_target + argument parser

./run.sh watch <profile> <subcmd> [args]

The second positional after \`watch\` is now the profile name. apply_target
calls load_profile + verify_serial_allowed before Gradle, so a wrong-watch
install refuses fast without burning a build. DOCTATE_API_KEY pass-through
is gone — keys come from the profile file only.

refs #3"
```

---

### Task 4: Rewrite the help-text block and drop deprecated env vars

The help/usage comment block at the top of `run.sh` still references the old `DOCTATE_DEV_SERVER`, `DOCTATE_API_KEY`, and the `(server=$DOCTATE_DEV_SERVER)` watch-target description. This task aligns the documentation with the new CLI form and removes the now-unused `DEFAULT_DEV_SERVER` constant.

**Files:**
- Modify: `clients/wearos/run.sh` (the leading comment block, lines 1–55, plus the `DEFAULT_DEV_SERVER` line around 64)

- [ ] **Step 1: Replace the help-text block**

Replace lines 1–55 (everything from `#!/usr/bin/env bash` through the last `# SKIP_INSTRUMENTED set to 1 ...` line, inclusive) with:

```bash
#!/usr/bin/env bash
# Build / install / run helper for the Wear OS app.
#
# Usage:
#   ./run.sh <target> [profile] <subcommand> [args]
#
# A target prefix is REQUIRED for every device-bound subcommand. The script
# never silently picks "the first attached device" — if more than one candidate
# exists it asks, and if ADB_SERIAL/WEAR_AVD pins a choice it uses that.
#
# Target prefix (or DOCTATE_TARGET env var):
#   watch <profile>
#                 Real Pixel Watch over ADB-WiFi. <profile> is the basename of
#                 a file in profiles.d/<profile>.sh that holds SERVER_URL,
#                 API_KEY, and ALLOWED_SERIALS (see profiles.d/*.sh.example).
#                 The build refuses to proceed if the attached watch's ADB
#                 serial is not listed in the profile's ALLOWED_SERIALS — this
#                 protects against installing the wrong key on the wrong watch.
#   emulator      Wear OS AVD on this host. No profile — uses the hardcoded
#                 emulator-loopback URL (10.0.2.2:3000) and the api_key from
#                 the profile file 'emulator-default.sh' if present, otherwise
#                 the build's MISSING_API_KEY sentinel.
#                 - 0 emulators running → menu of AVDs from `emulator -list-avds`
#                 - ≥1 running          → menu of running serials + "Start a new AVD"
#                 To bypass the menu pin the choice via ADB_SERIAL=<serial> or
#                 WEAR_AVD=<name>.
#
# Subcommands that need a target:
#   (none)        install + start (default inner loop)
#   build         ./gradlew :app:assembleDebug
#   install       ./gradlew :app:installDebug
#   start         adb shell am start ...
#   stop          adb shell am force-stop ...
#   logcat        adb logcat, filtered to the app's PID
#   shot [path]   screencap to PNG (default /tmp/wear_screen.png)
#   test          run JVM unit tests, plus instrumented tests on the target
#                 (set SKIP_INSTRUMENTED=1 to skip the on-device tier)
#
# Target-free subcommands (run without a prefix):
#   devices       list attached devices
#   connect [host:port]
#                 adb connect to a Pixel Watch over WiFi and remember
#                 the serial in .watch_serial. Without an argument:
#                 reuse the cached serial.
#   clean         ./gradlew clean
#   help          show this help
#
# Env vars:
#   DOCTATE_TARGET    Same as the <target> prefix (watch|emulator).
#                     The CLI prefix takes precedence when both are set.
#   DOCTATE_PROFILE   Same as the [profile] positional. The CLI value wins.
#   ADB_SERIAL        Pin a specific device serial. Skips the menu entirely
#                     AND the profile allow-list (it is still verified — pinning
#                     to a non-allowed serial dies with the same refusal).
#   WEAR_AVD          AVD name to auto-boot when no emulator is attached
#                     (skips the AVD menu; useful for CI / scripted runs).
#   SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no device needed).
```

(Note: the line about `emulator-default.sh` is forward-looking — the emulator path keeps its hardcoded URL today; if you later want emulator-specific keys, add `profiles.d/emulator-default.sh` and adapt the emulator branch in `apply_target`. Out of scope for this plan.)

- [ ] **Step 2: Remove the DEFAULT_DEV_SERVER constant**

Find this line (around line 64):

```bash
DEFAULT_DEV_SERVER="http://192.168.178.27:3000"
```

Delete it. It is no longer referenced (Task 3 already removed the use site).

- [ ] **Step 3: Verify the script still parses and the help works**

```bash
./run.sh help | head -20
```

Expected: the new help text from Step 1, no shell-parse errors.

- [ ] **Step 4: Verify no remaining references to deprecated symbols**

```bash
grep -nE "DOCTATE_DEV_SERVER|DOCTATE_API_KEY|DEFAULT_DEV_SERVER" clients/wearos/run.sh
```

Expected: no matches.

- [ ] **Step 5: Commit**

```bash
git add clients/wearos/run.sh
git commit -m "docs(wear): rewrite run.sh help-text for profile-based builds

Drop DOCTATE_DEV_SERVER, DOCTATE_API_KEY, and DEFAULT_DEV_SERVER — the
profile file is the only source for URL + key now.

refs #3"
```

---

### Task 5: Clean up local.properties

The `doctate.apiKey=...` line in `clients/wearos/local.properties` was the sticky per-developer default for the old single-profile setup. With profiles canonical, it becomes a confusing second source of truth. The `build.gradle.kts` fallback chain (Gradle property → local.properties → hardcoded sentinel) still reads it if present, so leaving stale values in there would silently override an unset profile API_KEY in some failure modes.

**Files:**
- Modify: `clients/wearos/local.properties` (this file is gitignored, so the change is local-only — the commit only touches build.gradle.kts in Step 3)
- Modify: `clients/wearos/app/build.gradle.kts` to drop the `local.properties` fallback for `doctate.apiKey` and `doctate.serverUrl`

- [ ] **Step 1: Inspect the current local.properties**

```bash
cat clients/wearos/local.properties
```

If it contains `doctate.apiKey=...` and nothing else, delete the whole file. If it contains other keys (e.g. `sdk.dir=...`), edit it and delete only the `doctate.apiKey` and any `doctate.serverUrl` lines, keeping the rest.

- [ ] **Step 2: Update build.gradle.kts to drop the local.properties fallback for doctate.* keys**

Find this block in `clients/wearos/app/build.gradle.kts`:

```kotlin
        // Build-time config resolved in this order:
        //   1. Gradle CLI property (-Pdoctate.serverUrl=...) — used by run.sh
        //      to switch between emulator and watch builds without touching
        //      local.properties.
        //   2. local.properties (gitignored) — sticky per-developer default.
        //   3. Hardcoded fallback — emulator loopback + sentinel key.
        val localProps = Properties().apply {
            rootProject.file("local.properties").takeIf { it.exists() }
                ?.inputStream()?.use { load(it) }
        }
        fun resolveProp(key: String, default: String): String =
            (project.findProperty(key) as? String)
                ?: localProps.getProperty(key)
                ?: default
        buildConfigField(
            "String", "SERVER_URL",
            "\"${resolveProp("doctate.serverUrl", "http://10.0.2.2:3000")}\""
        )
        buildConfigField(
            "String", "API_KEY",
            "\"${resolveProp("doctate.apiKey", "MISSING_API_KEY")}\""
        )
```

Replace it with:

```kotlin
        // Build-time config resolved in this order:
        //   1. Gradle CLI property (-Pdoctate.serverUrl=...) — set by run.sh
        //      from the active profile (profiles.d/<name>.sh).
        //   2. Hardcoded fallback — emulator loopback + sentinel key. This
        //      path is only taken for the `emulator` target, which has no
        //      profile concept.
        fun resolveProp(key: String, default: String): String =
            (project.findProperty(key) as? String) ?: default
        buildConfigField(
            "String", "SERVER_URL",
            "\"${resolveProp("doctate.serverUrl", "http://10.0.2.2:3000")}\""
        )
        buildConfigField(
            "String", "API_KEY",
            "\"${resolveProp("doctate.apiKey", "MISSING_API_KEY")}\""
        )
```

Also remove the now-unused import at the top of the file:

```kotlin
import java.util.Properties
```

- [ ] **Step 3: Verify the watch build still configures**

From `clients/wearos/`:

```bash
./gradlew :app:tasks --quiet | head -5
```

Expected: no Kotlin compile errors, task list prints. (We don't run a full build yet — that comes in Task 6.)

- [ ] **Step 4: Commit**

```bash
git add clients/wearos/app/build.gradle.kts
git commit -m "refactor(wear): drop local.properties fallback for doctate.* keys

Profile file (via -Pdoctate.* injected by run.sh) is now the only source.
local.properties stays gitignored but is no longer consulted by the build.

refs #3"
```

---

### Task 6: Mid-implementation smoke-test — install brummel-dev on the watch

Before adding the other two profiles, prove the brummel-dev path works end-to-end against a real watch. This is the canary build.

**Files:** none modified — verification only.

**Precondition:** Brummel's watch is connected via ADB-WiFi (run `./run.sh connect` from `clients/wearos/` if not already), and the laptop dev server is running on `192.168.178.27:3000`.

- [ ] **Step 1: Verify the watch is attached**

From `clients/wearos/`:

```bash
./run.sh devices
```

Expected: at least Brummel's watch serial in the listing.

- [ ] **Step 2: Install with brummel-dev**

```bash
./run.sh watch brummel-dev install
```

Expected: `Target: watch (profile=brummel-dev, serial=adb-XXX..., server=http://192.168.178.27:3000)` log line, Gradle build completes, install succeeds. **If `verify_serial_allowed` dies** at this step, the watch serial in `profiles.d/brummel-dev.sh` does not match — fix the profile file and retry.

- [ ] **Step 3: Start the app and confirm it can reach the dev server**

```bash
./run.sh watch brummel-dev start
```

Then on the watch: open the app, observe the case list loads (oneliners come back from the server). On the laptop:

```bash
tail -n 50 ~/.local/share/doctate/logs/server.log
# or wherever the dev-server logs go — adjust to your setup
```

Expected: a 200 response for a `GET /api/oneliners` from the watch's IP within a few seconds of opening the app.

- [ ] **Step 4: Verify the cross-watch refusal (if you have access to a second watch right now)**

If a second watch (or any other ADB-WiFi device) is also connected, run:

```bash
ADB_SERIAL=<other-device-serial> ./run.sh watch brummel-dev install
```

Expected: hard refusal with the `verify_serial_allowed` error message naming both the attached serial and the allow-list. If you do not have a second watch attached, skip this step — Task 11 will cover it as part of the full matrix.

- [ ] **Step 5: No commit (verification only)**

If the smoke-test passes, proceed to Task 7. If it fails, debug the failure before continuing — fixes to `run.sh` get folded into Task 2 or Task 3 retroactively (squash via interactive rebase or amend the most recent commit if not yet pushed).

---

### Task 7: Add the remaining profiles (brummel-minerva, krey-minerva)

**Files:**
- Create: `clients/wearos/profiles.d/brummel-minerva.sh` (gitignored)
- Create: `clients/wearos/profiles.d/krey-minerva.sh` (gitignored)

- [ ] **Step 1: Create brummel-minerva.sh**

```bash
cat > clients/wearos/profiles.d/brummel-minerva.sh <<'EOF'
SERVER_URL="http://minerva.lan:3000"
API_KEY="<plaintext from Prereq 1 — minerva's users.toml under slug 'brummel'>"
ALLOWED_SERIALS=(
    "<Brummel watch serial from Prereq 3 — same as in brummel-dev.sh>"
)
EOF
```

Edit the file to replace both `<...>` placeholders with the real values from Prereq 1 and Prereq 3.

- [ ] **Step 2: Create krey-minerva.sh**

```bash
cat > clients/wearos/profiles.d/krey-minerva.sh <<'EOF'
SERVER_URL="http://minerva.lan:3000"
API_KEY="<plaintext from Prereq 2 — minerva's users.toml under slug 'krey'>"
ALLOWED_SERIALS=(
    "<Krey watch serial from Prereq 3>"
)
EOF
```

Edit the file to replace both `<...>` placeholders.

- [ ] **Step 3: Verify both files are gitignored**

```bash
git status clients/wearos/profiles.d/
```

Expected: neither `brummel-minerva.sh` nor `krey-minerva.sh` shows up as untracked.

- [ ] **Step 4: Verify load_profile sees both new profiles**

```bash
./run.sh watch bogus install 2>&1 | head -3
```

Expected: the `Available:` line in the error now lists `brummel-dev brummel-minerva krey-minerva`.

- [ ] **Step 5: No commit (profile files are local-only)**

---

### Task 8: Add PROFILE_NAME, IS_DEV_PROFILE, and appLabel placeholder

Inject the profile name into the build and derive a `IS_DEV_PROFILE` boolean and the launcher label placeholder.

**Files:**
- Modify: `clients/wearos/app/build.gradle.kts`

- [ ] **Step 1: Resolve profileName and derive isDevProfile**

In `clients/wearos/app/build.gradle.kts`, find the block from Task 5 Step 2 (the one with `fun resolveProp` and the two existing `buildConfigField` calls). Immediately above the first `buildConfigField` call, add:

```kotlin
        val profileName = resolveProp("doctate.profileName", "unknown")
        val isDevProfile = profileName.endsWith("-dev")
```

- [ ] **Step 2: Add the two new BuildConfig fields**

Immediately after the existing `buildConfigField("String", "API_KEY", ...)` call, add:

```kotlin
        buildConfigField(
            "String", "PROFILE_NAME",
            "\"$profileName\""
        )
        buildConfigField(
            "boolean", "IS_DEV_PROFILE",
            "$isDevProfile"
        )
```

- [ ] **Step 3: Set the manifest placeholder for the app label**

Still inside `defaultConfig { ... }`, immediately after the four `buildConfigField` calls, add:

```kotlin
        manifestPlaceholders["appLabel"] = if (isDevProfile) "Doctate (Dev)" else "Doctate"
```

- [ ] **Step 4: Verify the configuration compiles**

```bash
./run.sh watch brummel-dev build
```

Expected: build succeeds. The generated `BuildConfig.java` (under `clients/wearos/app/build/generated/source/buildConfig/...`) should contain `PROFILE_NAME = "brummel-dev"` and `IS_DEV_PROFILE = true`.

- [ ] **Step 5: Verify BuildConfig values for a non-dev profile**

```bash
./run.sh watch brummel-minerva build
grep -nE 'PROFILE_NAME|IS_DEV_PROFILE' \
    clients/wearos/app/build/generated/source/buildConfig/debug/com/doctate/watch/BuildConfig.java
```

Expected: `public static final String PROFILE_NAME = "brummel-minerva";` and `public static final boolean IS_DEV_PROFILE = false;`.

- [ ] **Step 6: Commit**

```bash
git add clients/wearos/app/build.gradle.kts
git commit -m "feat(wear): expose PROFILE_NAME and IS_DEV_PROFILE to the app

BuildConfig now carries the active profile name plus an isDev boolean
(profile name ends in '-dev'). The manifestPlaceholder 'appLabel'
switches between 'Doctate' and 'Doctate (Dev)' for the launcher.

refs #3"
```

---

### Task 9: Wire appLabel placeholder into AndroidManifest.xml

**Files:**
- Modify: `clients/wearos/app/src/main/AndroidManifest.xml`

- [ ] **Step 1: Replace the hardcoded app label**

In `clients/wearos/app/src/main/AndroidManifest.xml`, find:

```xml
        android:label="@string/app_name"
```

(This is in the `<application>` tag, around line 18.)

Replace it with:

```xml
        android:label="${appLabel}"
```

Leave the `android:label="@string/tile_label"` on the tile service and `android:label="@string/app_name"` on the complication service unchanged — those identify the tile/complication choices in the system UI, not the launcher icon.

- [ ] **Step 2: Verify the build produces the right manifest for brummel-dev**

```bash
./run.sh watch brummel-dev build
unzip -p clients/wearos/app/build/outputs/apk/debug/app-debug.apk AndroidManifest.xml \
    | aapt2 dump xmltree --file - 2>/dev/null \
    | grep -A1 'application' | head -20
```

If `aapt2` is not on PATH, fall back to:

```bash
"$HOME/Android/Sdk/build-tools/"*/aapt2 dump xmltree clients/wearos/app/build/outputs/apk/debug/app-debug.apk --file AndroidManifest.xml \
    | grep -A1 'application' | head -20
```

Expected: a `label` attribute with the value `Doctate (Dev)` in the application element.

- [ ] **Step 3: Verify for brummel-minerva**

```bash
./run.sh watch brummel-minerva build
"$HOME/Android/Sdk/build-tools/"*/aapt2 dump xmltree clients/wearos/app/build/outputs/apk/debug/app-debug.apk --file AndroidManifest.xml \
    | grep -A1 'application' | head -20
```

Expected: `label` attribute is `Doctate` (no `(Dev)` suffix).

- [ ] **Step 4: Install on the watch and confirm the launcher label visually**

```bash
./run.sh watch brummel-dev install
```

Then on the watch: open the app drawer, look for the Doctate icon — its label should read `Doctate (Dev)`.

Reinstall with the other profile:

```bash
./run.sh watch brummel-minerva install
```

App drawer should now show `Doctate` without the suffix.

- [ ] **Step 5: Commit**

```bash
git add clients/wearos/app/src/main/AndroidManifest.xml
git commit -m "feat(wear): use appLabel placeholder for launcher label

Dev profiles (name ends in -dev) get 'Doctate (Dev)' in the launcher;
production profiles keep the plain 'Doctate'. The string resource
@string/app_name is no longer the application-level label, only used
for the complication entry where the launcher distinction is irrelevant.

refs #3"
```

---

### Task 10 (OPTIONAL): DevBadge composable and integration

This task is the visual-badge layer from the spec. It is classified as **nice-to-have** — the hard-lock in `run.sh` already prevents the worst-case misinstall. The badge protects against the softer case of Brummel forgetting which profile is currently active. Skip the whole task if you want to ship faster.

**Files:**
- Create: `clients/wearos/app/src/main/java/com/doctate/watch/presentation/components/DevBadge.kt`
- Modify: `clients/wearos/app/src/main/java/com/doctate/watch/presentation/CaseListScreen.kt`
- Modify: `clients/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingScreen.kt`

- [ ] **Step 1: Create the DevBadge composable**

Create `clients/wearos/app/src/main/java/com/doctate/watch/presentation/components/DevBadge.kt`:

```kotlin
package com.doctate.watch.presentation.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.Text
import com.doctate.watch.BuildConfig

@Composable
fun DevBadge(modifier: Modifier = Modifier) {
    if (!BuildConfig.IS_DEV_PROFILE) return
    Text(
        text = "DEV",
        color = Color(0xFFFF5252),
        style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold),
        modifier = modifier
            .background(Color.Black.copy(alpha = 0.6f), RoundedCornerShape(4.dp))
            .padding(horizontal = 4.dp, vertical = 1.dp),
    )
}
```

- [ ] **Step 2: Mount in CaseListScreen**

In `clients/wearos/app/src/main/java/com/doctate/watch/presentation/CaseListScreen.kt`, add this import alongside the existing imports (alphabetical order — between `com.doctate.watch.DoctateApp` and `com.doctate.watch.domain.CaseEntry`):

```kotlin
import com.doctate.watch.presentation.components.DevBadge
```

The `ScreenScaffold`'s `content` slot is a `BoxScope` (verifiable: the existing code in `RecordingScreen.kt` uses `Modifier.align(Alignment.BottomCenter)` on the `EdgeButton` inside `ScreenScaffold`). So `Alignment.TopEnd` works directly.

Find the line `) { contentPadding ->` (around line 63). Insert the badge as the FIRST composable inside that lambda, immediately after the `{ contentPadding ->` opening:

```kotlin
    ) { contentPadding ->
        DevBadge(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .padding(end = 4.dp, top = 2.dp),
        )
        if (displayed.isEmpty()) {
```

This places the badge at the top-right corner of the screen, layered on top of the list content. In a non-dev build the composable returns immediately and renders nothing.

- [ ] **Step 3: Mount in RecordingScreen**

Open `clients/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingScreen.kt`. Add the import alongside the existing imports (alphabetical order — after `androidx.wear.compose.material3.Text`):

```kotlin
import com.doctate.watch.presentation.components.DevBadge
```

Find the line `ScreenScaffold { contentPadding ->` (around line 81). Insert the badge as the FIRST composable inside that lambda, immediately after the `{ contentPadding ->` opening, before the existing `Column(...)`:

```kotlin
    ScreenScaffold { contentPadding ->
        DevBadge(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .padding(end = 4.dp, top = 2.dp),
        )
        Column(
            modifier = Modifier
                .fillMaxSize()
```

Same alignment idiom as CaseListScreen — top-right corner, layered above the recording counter and the EdgeButton. No-op in production builds.

- [ ] **Step 4: Build + visually verify both profiles**

```bash
./run.sh watch brummel-dev install
```

Open the app on the watch. Expected: red "DEV" badge visible in the top-right of both the case list and the recording screen.

```bash
./run.sh watch brummel-minerva install
```

Reopen the app. Expected: no badge visible anywhere — UI looks production-clean.

- [ ] **Step 5: Commit**

```bash
git add clients/wearos/app/src/main/java/com/doctate/watch/presentation/components/DevBadge.kt \
        clients/wearos/app/src/main/java/com/doctate/watch/presentation/CaseListScreen.kt \
        clients/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingScreen.kt
git commit -m "feat(wear): add DevBadge visible only in dev-profile builds

Small red 'DEV' tag in the top-right of CaseListScreen and RecordingScreen
when BuildConfig.IS_DEV_PROFILE is true. Production profiles render
nothing — Krey's UI stays clean.

refs #3"
```

---

### Task 11: Full smoke-test matrix

Run the full spec matrix end-to-end. **Prereq 1 and Prereq 2 must be complete** before any minerva-server row in this matrix can pass.

**Files:** none modified — verification only.

- [ ] **Step 1: Row 1 — brummel-dev on Brummel's watch (happy path)**

Watch attached: Brummel's.

```bash
./run.sh watch brummel-dev install && ./run.sh watch brummel-dev start
```

Expected:
- Install succeeds.
- App reaches the laptop dev server at `192.168.178.27:3000`.
- Server log shows 200 on `/api/oneliners` from the watch.
- Launcher label is `Doctate (Dev)`.
- (Optional) `DEV` badge visible in the UI.

- [ ] **Step 2: Row 2 — brummel-dev on Krey's watch (hard refusal)**

Watch attached: Krey's (or any device whose serial is NOT in `profiles.d/brummel-dev.sh::ALLOWED_SERIALS`).

```bash
./run.sh watch brummel-dev install 2>&1 | tail -10
```

Expected: hard refusal with the `verify_serial_allowed` error, listing both the attached serial and the allow-list, no Gradle invocation.

- [ ] **Step 3: Row 3 — brummel-minerva on Brummel's watch**

Watch attached: Brummel's.

```bash
./run.sh watch brummel-minerva install && ./run.sh watch brummel-minerva start
```

Expected:
- Install succeeds.
- App reaches `minerva.lan:3000` (verify via the minerva server's container log).
- Launcher label is plain `Doctate` (no `(Dev)` suffix).
- No DEV badge.

- [ ] **Step 4: Row 4 — krey-minerva on Krey's watch**

Watch attached: Krey's.

```bash
./run.sh watch krey-minerva install && ./run.sh watch krey-minerva start
```

Expected:
- Install succeeds.
- App reaches `minerva.lan:3000` and authenticates as slug `krey` (verify in the minerva server log that the X-API-Key resolves to `krey`).
- Launcher label is plain `Doctate`.

- [ ] **Step 5: Row 5 — krey-minerva on Brummel's watch (hard refusal)**

Watch attached: Brummel's.

```bash
./run.sh watch krey-minerva install 2>&1 | tail -10
```

Expected: hard refusal — Brummel's serial is not in `krey-minerva.sh::ALLOWED_SERIALS`.

- [ ] **Step 6: Row 6 — unknown profile**

```bash
./run.sh watch nonexistent install 2>&1 | tail -5
```

Expected: error listing available profiles (`brummel-dev brummel-minerva krey-minerva`).

- [ ] **Step 7: Row 7 — missing subcommand after profile**

```bash
./run.sh watch brummel-dev 2>&1 | tail -5
```

Expected: the default no-subcommand path runs `cmd_install; cmd_start`. (This is by design — the bare-watch path was "install + start" before, the profile positional doesn't change that. If you'd rather see an error here, that's a separate design tweak.)

- [ ] **Step 8: Row 8 — emulator path unchanged**

```bash
./run.sh emulator install
```

(Requires a Wear OS AVD; pick one from the menu when prompted.)

Expected: build + install succeeds, the emulator path is unaffected, no profile required.

- [ ] **Step 9: Wrap-up**

If all rows pass: this plan is done. Close Issue #3 with a comment linking the merged commit range (`git log --oneline <first-task-1-commit>..HEAD -- clients/wearos`).

If any row fails: open a new commit fixing the regression, retry the affected row, and re-run the full matrix on green.

- [ ] **Step 10: Push**

```bash
git push origin main
```

(Or whichever branch this work lives on if not main — adjust accordingly.)
