# PropertyLab Sales Agent — Android-first Flutter implementation plan

Date: 2026-08-21

Design authority: `docs/superpowers/specs/2026-08-21-propertylab-sales-agent-flutter-design.md`

Status: ready to execute

## Goal

Deliver a separately installable Android Pilot of **PropertyLab Sales Agent**
that lets an authenticated petaV3 salesperson search the approved full Lead
scope, select a Lead, start and stop a YHY02 recording from either the app or
badge button, launch Phone or WhatsApp, and automatically upload exactly one
AAC recording that petaV3 deterministically links to the selected Lead.

The Flutter repository must keep the shared product logic platform-neutral so
that Swift adapters can provide the same iOS behavior after Android is stable.

## Repositories and branches

### petaV3 server

- Repository: `/Users/dadadineiyou/Documents/GitHub/petav3`
- Current feature branch: `feat/agent-ble-upload`
- Local integration branch: `dev-chen`
- Integration worktree: `/Users/dadadineiyou/Documents/GitHub/petav3-dev-chen-integration`
- Do not push.
- After every clean petaV3 task commit, cherry-pick only that commit into the
  clean local `dev-chen` worktree. Stop if that worktree is dirty or the
  cherry-pick is ambiguous.

### Flutter client

- New repository: `/Users/dadadineiyou/Documents/GitHub/propertylab-sales-agent`
- Initial branch: `codex/android-pilot`
- Product name: `PropertyLab Sales Agent`
- Android Pilot application ID: `tech.propertylab.salesagent.pilot`
- Android Production application ID: `tech.propertylab.agent`
- Proposed iOS bundle ID: `tech.propertylab.salesagent`
- Do not push.

### Read-only references

- Kotlin Android reference: `/Users/dadadineiyou/propertylab-android`
- YHY02 protocol authority:
  `/Users/dadadineiyou/Desktop/YHY BLE/Recording Card Wi-Fi+BLE – Protocol Specification.pdf`
- Validated hardware notes:
  `/Users/dadadineiyou/Desktop/YHY BLE/WINDOWS-VALIDATION-2026-08-18.md`
  and `/Users/dadadineiyou/Desktop/YHY BLE/HANDOFF-YHY02-Windows.md`
- Brand source:
  `/Users/dadadineiyou/Desktop/小程序办理材料/miniapp.png`

The Kotlin repository supplies tested behavior and real-device golden frames;
it is not modified by this plan and is not imported wholesale into Flutter.

## Hard constraints

1. The new app uses petaV3 only. It contains no petaV2 URL, credential, token,
   fallback, or compatibility path.
2. YHY02 production command dispatch is allow-listed. Never send `0x0043`
   delete, `0x0032` format, `0x0030` hotspot, OTA, or undocumented commands.
3. Recording control exposes only documented FEE3 `0x0022` Start/Stop.
4. Upload success deletes the private phone cache only after the durable petaV3
   response is committed locally. The badge copy is retained.
5. A Lead must be selected before a normal physical-button recording start.
   Unexpected starts become unassigned and cannot upload until assigned.
6. The old Kotlin app and Flutter Pilot may coexist on a phone but must never
   simultaneously own the same badge BLE connection/service.
7. Do not add dependencies outside the list in this plan without stopping for
   review. Resolve current Flutter-compatible stable versions in Task 8 and
   commit `pubspec.lock`; do not silently upgrade them during later tasks.
8. All API identity comes from JWT. The client never supplies a trusted Admin
   ID or internal Lead ID.
9. Server interaction identity reuses `agent_call_events.external_id` as
   `flutter:<interaction_uuid>`; do not add a redundant UUID column.
10. A normal Lead-linked start requires the server to acknowledge that same
    interaction UUID before any BLE Start or external launch. Offline
    unexpected recordings remain unassigned until explicit online assignment.
11. Android native capture must not rely on a cached Flutter engine surviving
    process death. User force-stop is recovered only after the next explicit
    app launch and badge-history reconciliation.
12. No implementation task may skip RED → observed failure → minimal GREEN → relevant full
    regression → atomic commit.

## Implementation architecture

Flutter follows the official View → ViewModel → Repository → Service split,
with a small domain layer only where the recording/interaction state machine
needs platform-independent rules.

```text
Flutter views/view models
        ↓
InteractionCoordinator · UploadCoordinator
        ↓
repositories (API + Drift ledger)
        ↓
services/platform contracts
        ↓
Android Kotlin now · iOS Swift later
```

The YHY02 integration is an internal Flutter plugin at
`packages/yhy02_badge`. Pigeon generates typed messages between Dart and the
native adapters. Pure framing, ADTS validation, state transitions, and upload
classification stay in Dart and run without a device.

Planned Flutter dependencies, with compatible stable versions locked in Task
8:

- runtime: `dio`, `drift`, the stable Drift SQLite runtime recommended by the
  current Drift setup guide, `flutter_secure_storage`, `path_provider`,
  `url_launcher`, `crypto`, `uuid`, `provider`, and `workmanager`;
- development: `drift_dev`, `build_runner`, `pigeon`, `flutter_lints`,
  `flutter_launcher_icons`, and SDK `integration_test`.

Approved Android runtime/test dependencies are AndroidX Core, Lifecycle,
WorkManager, JUnit, AndroidX Test, Robolectric, and `work-testing`. Resolve and
lock compatible stable versions in the scaffold task. Pure Kotlin tests,
Robolectric lifecycle/worker tests, and real-device instrumentation are
separate gates; no JVM test is treated as proof of an OS lifecycle behavior.

Do not introduce Freezed, a routing framework, a service locator, or a second
state-management package for the first release. Plain immutable Dart models,
`ChangeNotifier`, and `Navigator` are sufficient for the approved screens.

## Execution protocol

Execute in the exact written order, including lettered split tasks. For each
implementation task:

1. Give a fresh implementer the complete task text, the approved design, the
   hard constraints above, and the previous task SHA/context.
2. Implementer writes the failing test first and records the expected failure.
3. Implementer makes only the minimum implementation required, runs the target
   test and the stated regression, and creates exactly the listed commit.
4. A fresh spec reviewer checks behavior against this plan and design.
5. After spec approval, a fresh code-quality reviewer checks safety,
   maintainability, tests, and task scope.
6. Fix findings and repeat both reviews until both approve. Only then proceed.

Tasks 22–24 are the separately scheduled iOS follow-on and do not block the
Android Pilot acceptance.

---

## Task 1 — Persist Flutter interaction state in `agent_call_events`

**Repository:** petaV3

**Files:**

- Create
  `database/migrations/2026_08_21_100001_add_flutter_fields_to_agent_call_events_table.php`
- Modify `src/Call/AgentCallEvent.php`
- Create `tests/Feature/Call/AgentCallEventFlutterStateTest.php`

**RED:** Add tests proving the model can persist the Flutter source, badge SN,
integer interaction status, integer start source, and nullable integer stop
source, immutable Lead/channel/badge snapshots, `last_activity_at`, and
`lease_expires_at`, while exposing constants for the exact allowed values.
Also test the active-capture lookup (preparing/waiting/recording/stopping only)
uses the intended Admin/source/status composite
index, that queued/uploading/matched/filtered rows do not block a new capture,
and that existing web and Android sources still work.

Run and observe the missing-column/constant failure:

```bash
vendor/bin/phpunit tests/Feature/Call/AgentCallEventFlutterStateTest.php
```

**GREEN:**

- Add nullable columns `interaction_status`, `start_source`, `stop_source`,
  `badge_sn`, `lead_display_snapshot`, `phone_snapshot`, `last_activity_at`,
  and `lease_expires_at`; keep legacy rows valid.
- Use small unsigned integer constants:
  - status: preparing, waiting, recording, stopping, queued, uploading,
    matched, canceled, expired, failed-permanent, filtered;
  - start source: app, device button;
  - stop source: app, device button, recovery.
- Add `SOURCE_FLUTTER_APP` without changing the meaning of existing sources.
- Add casts/fillable fields and string-code conversion helpers. The database
  stores integers; HTTP resources later expose strings.
- Add the composite index needed for Admin-scoped active Flutter lookup; do not
  rely on a full-table status scan.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/Call/AgentCallEventFlutterStateTest.php \
  tests/Feature/Call/AgentCallEventRepositoryTest.php \
  tests/Feature/AgentApi/AgentCallEventIngestTest.php
```

**Commit:** `feat(agent-app): persist Flutter interaction state`

## Task 2 — Add an isolated mobile full-Lead-search permission

**Repository:** petaV3

**Files:**

- Modify `src/Auth/Permission.php`
- Modify `app/Actions/SeedCommonRolesAction.php`
- Create
  `database/migrations/2026_08_21_100002_grant_agent_app_lead_search_permission.php`
- Create `tests/Feature/Auth/AgentAppLeadSearchPermissionTest.php`
- Modify the narrow role/permission tests required by the seed change

**RED:** Add tests proving:

- `search-agent-app-all-leads` exists and is granted to the approved Sales
  Agent role by both fresh seed and upgrade migration;
- it is not equivalent to `VIEW_LEADS_ALL`;
- a user holding only the new permission retains the same Web Lead visibility
  level as before; and
- unrelated roles receive no implicit grant.

```bash
vendor/bin/phpunit tests/Feature/Auth/AgentAppLeadSearchPermissionTest.php \
  tests/Feature/Lead/LeadVisibilityTest.php
```

**GREEN:** Add `Permission::SEARCH_AGENT_APP_ALL_LEADS`, a clear display label,
the exact seed grant, and an idempotent production migration. Do not add it to
the normal Web Leads scoped-view group and do not grant `VIEW_LEADS_ALL`.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/Auth/AgentAppLeadSearchPermissionTest.php \
  tests/Feature/Lead/LeadVisibilityTest.php
```

**Commit:** `feat(agent-app): add isolated Lead search permission`

## Task 3 — Expose authorized cursor-paginated Lead search

**Repository:** petaV3

**Files:**

- Create `app/Http/Requests/AgentApi/SearchAgentLeadsRequest.php`
- Create `app/Http/Controllers/AgentApi/AgentLeadsController.php`
- Create `src/Lead/Queries/AgentAppLeadSearchQuery.php`
- Modify `routes/agent-api.php`
- Add a dedicated throttle definition and audit event/listener using existing
  project conventions
- Create `tests/Feature/AgentApi/AgentLeadSearchTest.php`

**RED:** Cover unauthenticated and missing-permission responses; search by
case-insensitive name, normalized Malaysian/international phone digits, and
email; deterministic cursor pagination; individual UUID lookup; customer-only
results; merged/deleted/staff exclusion; rejection of empty/too-short queries;
separate minimum normalized phone digits; page cap; cursor expiry and binding
to user+query; per-user throttling; audit emission/abnormal enumeration signal;
and proof that the same user's Web visibility is unchanged.

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentLeadSearchTest.php
```

**GREEN:** Add JWT/staff/permission-protected routes:

```text
GET /api/agent-leads?q=&cursor=
GET /api/agent-leads/{lead_uuid}
```

Return only `lead_uuid`, display name, primary phone, email, Lead status,
assigned salesperson when available, and `updated_at`. Use server-side
normalization and a short-lived opaque cursor with deterministic tie-breaking.
Use a maximum page size of 25 and a dedicated per-user throttle. Never allow an
empty-query browse or unbounded dataset download.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentLeadSearchTest.php \
  tests/Feature/AgentApi/JwtGuardTest.php \
  tests/Feature/Lead/LeadVisibilityTest.php
```

**Commit:** `feat(agent-app): add authorized Lead search API`

## Task 3A — Add authenticated badge list and first-bind API

**Repository:** petaV3

**Files:**

- Create `app/Http/Requests/AgentApi/BindAgentBadgeRequest.php`
- Create `app/Http/Controllers/AgentApi/AgentBadgesController.php`
- Create `src/Device/Services/AgentBadgeBindingService.php`
- Modify `routes/agent-api.php`
- Create `tests/Feature/AgentApi/AgentBadgeBindingTest.php`

**RED:** Test authenticated list, uppercase SN normalization, idempotent bind to
the same Admin, first bind of an unowned badge, two-Admin concurrent claim with
one winner, unknown/non-inventory serial 404, owned-by-other 409,
inactive/conflicting device records, audit attribution, and proof that the app
cannot transfer or unbind another Admin's badge. A claimed serial must never be
reassigned by last-write-wins.

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentBadgeBindingTest.php
```

**GREEN:** Implement:

```text
GET  /api/agent-badges
POST /api/agent-badges/bind
```

The server derives Admin from JWT. First-bind uses a transaction and a lock on
the normalized pre-provisioned inventory device; it never creates an arbitrary
serial and succeeds only when unowned or already owned by the same Admin. The
first-release app may disconnect locally but cannot silently transfer server
ownership; manager transfer remains outside this API.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentBadgeBindingTest.php \
  tests/Feature/AgentApi/AgentRecordingUploadTest.php
```

**Commit:** `feat(agent-app): add safe badge binding API`

## Task 4 — Create and update interactions idempotently

**Repository:** petaV3

**Files:**

- Create `app/Http/Requests/AgentApi/StoreAgentCallInteractionRequest.php`
- Create `app/Http/Requests/AgentApi/UpdateAgentCallInteractionRequest.php`
- Create `app/Http/Controllers/AgentApi/AgentCallInteractionsController.php`
- Create `src/Call/Services/AgentCallInteractionService.php`
- Modify `routes/agent-api.php`
- Create `tests/Feature/AgentApi/AgentCallInteractionTest.php`

**RED:** Test create/show/update for:

- JWT-derived Admin, authorized Lead UUID, owned badge, normalized SN, allowed
  channel, and `external_id=flutter:<uuid>`;
- retrying the same UUID returns the same row;
- retrying the same UUID with different immutable Lead/channel/badge fields
  returns 409;
- concurrent/different UUID create while one active returns 409 plus the
  existing UUID and does not insert a second row;
- the create transaction locks the authenticated Admin row before active-row
  inspection;
- expired preparing/waiting rows are terminalized under that same lock before
  a new create, while fresh leases still conflict;
- exact ten-minute preparing/waiting leases; 24-hour recording/stopping lease
  heartbeat and stale recovery expiry; queued/uploading rows not blocking the
  next capture; and
- a stop/audio arriving inside the recovery lease reconciling idempotently,
  while audio arriving after 24 hours becomes unassigned and cannot silently
  reopen/match the expired row (including when a newer interaction exists);
  preservation of the stale event audit trail;
- Lead/badge/interaction ownership rejection;
- approved Lead display/phone snapshotting;
- only valid forward state transitions and idempotent repeat transitions, with
  PATCH protected by `lockForUpdate` or version CAS;
- concurrent cancel/start/stop PATCH cannot regress or lose a winning state;
- state regression and client attempts to set server-only matched/filtered
  outcomes rejected; and
- old web/Android events excluded from the Flutter one-active predicate.

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentCallInteractionTest.php
```

**GREEN:** Implement:

```text
POST  /api/agent-call-interactions
GET   /api/agent-call-interactions/{interaction_uuid}
PATCH /api/agent-call-interactions/{interaction_uuid}
```

Resolve Admin and Lead server-side. Lock the Admin row in the create
transaction, terminalize an expired stale active-capture row, then return 409
for a different active-capture UUID or insert the new event. Queued/uploading
and terminal filtered rows do not block the next call. Late audio outside the
24-hour recovery boundary requires an explicit new recovery interaction after
the current capture ends; it never reopens the expired row. Map public UUID to/from the prefixed
`external_id`; do not add another UUID column. Same-UUID idempotency compares
immutable fields. Centralize the transition/lease table, lock state changes,
refresh liveness only on valid evidence, and expose stable string states.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentCallInteractionTest.php \
  tests/Feature/AgentApi/AgentCallEventIngestTest.php
```

**Commit:** `feat(agent-app): add idempotent interaction API`

## Task 5 — Deterministically link recording, interaction, and Lead

**Repository:** petaV3

**Files:**

- Create
  `database/migrations/2026_08_21_100003_add_integrity_fields_to_call_recordings_table.php`
- Modify `app/Http/Requests/AgentApi/StoreRecordingRequest.php`
- Modify `app/Http/Controllers/AgentApi/AgentRecordingsController.php`
- Modify `src/Call/Repositories/AgentCallEventRepository.php`
- Create `src/Call/Services/AgentInteractionRecordingLinker.php`
- Modify `tests/Feature/AgentApi/AgentRecordingUploadTest.php`
- Create `tests/Feature/AgentApi/AgentInteractionRecordingUploadTest.php`

**RED:** Add tests for:

- a valid Flutter `interaction_uuid` requiring a matching SHA-256 (and vice
  versa), including lowercase/uppercase hash input;
- server-computed streamed hash mismatch rejection before durable linking;
- owned interaction/badge/Lead and direct Lead propagation to recording;
- transaction lock order: interaction first, recording second;
- first link, same-link retry, two recordings for one interaction conflict,
  and one recording for two interactions conflict;
- persisted server-computed SHA-256 and byte count;
- same `(device_sn,file_name)` plus same interaction/hash/size returns the
  original 2xx;
- same recording identity plus changed content/hash/size returns 409;
- an expired-outside-recovery interaction upload returns a non-matching
  conflict and never silently attaches the recording;
- same recording identity plus different interaction returns 409 and never
  rebinds;
- too-short upload produces durable `filtered`, no matched-audio claim, and no
  analysis job;
- race loser media cleanup still works; and
- accepted Flutter audio dispatches the existing analysis pipeline exactly
  once; and
- Flutter deterministic uploads bypass the legacy time matcher while legacy
  upload behavior stays unchanged.

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentInteractionRecordingUploadTest.php
```

**GREEN:** Extend multipart ingest with `interaction_uuid` and `audio_sha256`.
They are a required pair for the Flutter contract but remain jointly absent for
the existing Kotlin client during the compatibility window. Stream/hash the
temporary upload and persist hash plus byte count. In one transaction lock the
interaction then existing/new recording, create or accept only the same
interaction/hash/size link, set recording `lead_id`, and
advance the interaction to matched or filtered. Keep legacy uploads working
during rollout.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentInteractionRecordingUploadTest.php \
  tests/Feature/AgentApi/AgentRecordingUploadTest.php \
  tests/Feature/Call/AgentCallEventRepositoryTest.php
```

**Commit:** `feat(agent-app): link interaction recording and Lead`

## Task 6 — Audit recording links and prepare safe uniqueness rollout

**Repository:** petaV3

**Files:**

- Create `app/Console/Commands/AuditAgentCallRecordingLinks.php`
- Create `docs/agent-app/RECORDING-LINK-UNIQUE-INDEX-RUNBOOK.md`
- Create `tests/Feature/Call/AuditAgentCallRecordingLinksTest.php`

**RED:** Prove the audit reports duplicate non-null `call_recording_id` values
without mutating them, returns a failing exit status when duplicates exist, and
reports table/index/engine facts needed to choose an online-DDL strategy.

```bash
vendor/bin/phpunit tests/Feature/Call/AuditAgentCallRecordingLinksTest.php \
  tests/Feature/AgentApi/AgentInteractionRecordingUploadTest.php
```

**GREEN:** Implement the read-only audit and a runbook covering table size,
storage engine, online DDL support, write quiescence/maintenance window,
index-exists detection, retry, rollback, and post-DDL verification. Do not ship
a generic Laravel DDL migration that audits and alters in one deploy: that has
an audit/DDL TOCTOU and can lock a large production table. The Task 5
transactional invariant protects the Flutter path until a separately approved
online unique-index operation is safe. If duplicates exist, stop and create a
reviewed data-repair plan; never guess a winner.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/Call/AuditAgentCallRecordingLinksTest.php \
  tests/Feature/AgentApi/AgentInteractionRecordingUploadTest.php
```

**Commit:** `chore(agent-app): audit recording link uniqueness`

## Task 6A — Record and enforce the unique-index rollout decision

**Repository:** petaV3 plus authorized staging/production operations

**Files:**

- Create `docs/agent-app/RECORDING-LINK-UNIQUE-INDEX-DECISION.md`
- Add/extend a regression test enumerating every application write path to
  `agent_call_events.call_recording_id`

**RED:** Run the audit and write-path regression. The decision gate fails while
the audit is dirty, while a write path bypasses the Task 5 lock order, or while
neither an executed-index record nor a time-bounded owner-approved defer exists.

**GREEN:** With explicit environment-owner authorization, choose exactly one:

1. execute the runbook's online/maintenance-window unique-index operation and
   record preflight, command, timing, index verification, rollback evidence; or
2. record a Pilot-only defer with named owner, concrete risk, deadline, and
   proof that every current write path shares the transactional invariant.

Do not push or run DDL without authorization. A silent/indefinite defer is
BLOCKED, not DONE. Production Task 21 must execute the index or record a newly
approved bounded decision; it cannot inherit an expired Pilot defer.

**Regression:** Re-run audit, write-path tests, and index introspection when the
index path is chosen.

**Commit:** `docs(agent-app): record recording-link index decision`

## Task 7 — Document and regression-test the petaV3 contract

**Repository:** petaV3

**Files:**

- Create `docs/agent-app/PROPERTYLAB-SALES-AGENT-API.md`
- Create `tests/Feature/AgentApi/AgentAppContractTest.php`
- Modify `routes/agent-api.php` comments that still describe an Android-only or
  petaV2-dependent companion app

**RED:** Write a compact contract test exercising login, Lead search, badge
first-bind/list, interaction create/start/stop/heartbeat, active conflict,
recording upload, duplicate retry, and final interaction/recording/Lead linkage.

```bash
vendor/bin/phpunit tests/Feature/AgentApi/AgentAppContractTest.php
```

**GREEN:** Document requests, responses, transition codes, 401 refresh,
409 conflicts, durable `filtered`, idempotency, upload hash, and rollout
compatibility. Remove inaccurate route comments without refactoring unrelated
routes.

**Regression:**

```bash
vendor/bin/phpunit tests/Feature/AgentApi \
  tests/Feature/Call/AgentCallEventFlutterStateTest.php
```

Then run the complete petaV3 suite before considering the server contract
ready for staging:

```bash
php artisan test
```

**Commit:** `docs(agent-app): publish mobile API contract`

### Server staging gate

After Task 7 is approved, integrate its final petaV3 commit into the clean
local `dev-chen` worktree and produce a staging migration/deployment checklist
with the exact nine task SHAs. Do not push or deploy without explicit owner
authorization. Flutter Tasks 8 through 19B may continue against fakes/local petaV3, but
Task 20 real end-to-end acceptance is blocked until the reviewed server
commits are deployed to the approved petaV3 staging environment and the Task 6
duplicate-link audit is clean.

## Task 8 — Scaffold the Flutter repository and build flavors

**Repository:** Flutter client

**Files:**

- Create the Flutter application with Android and iOS platform folders
- Create `lib/app/app.dart`, `lib/app/bootstrap.dart`, and `lib/main.dart`
- Create `lib/main_pilot.dart` and `lib/main_production.dart`
- Add `android/app/src/pilot`, `android/app/src/production`, and flavor config
- Add shared iOS Pilot/Production Xcode schemes, xcconfig files, bundle IDs,
  display names, and a scheme-list verification script
- Add Android `dataExtractionRules`/`fullBackupContent` exclusions for token,
  Drift, AAC, and the native capture journal
- Add `analysis_options.yaml`, `pubspec.yaml`, `pubspec.lock`, `.gitignore`,
  `README.md`, and `tool/verify_no_petav2.sh`
- Add `test/app/bootstrap_test.dart`

**Preflight:** Record but do not upgrade the installed toolchain:

```bash
flutter --version
/usr/libexec/java_home -v 17
flutter doctor -v
```

Pin the repository to the installed Flutter 3.41.9/Dart 3.11.5 baseline and
run Android commands with `JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home`.

**RED:** Start with a bootstrap test requiring product/flavor identity and a
nonempty HTTPS API base URL supplied at build time. Run it before implementing
bootstrap and observe failure. Add a manifest/resource test that fails until
private recording/token/database/journal paths are excluded from backup, and a
script test that fails until both shared Xcode schemes are discoverable.

```bash
flutter test test/app/bootstrap_test.dart
```

**GREEN:** Create the repository, resolve only the approved Flutter and Android
runtime/test dependencies, commit Flutter and Gradle lock/version-catalog
state, and configure Pilot/Production. Production must not ship a
localhost or petaV2 default; missing base URL fails the build/bootstrap. Pilot
and Production labels/package IDs are visibly different. Backup exclusion is a
build-tested resource, not a documentation promise. iOS Pilot/Production
schemes are shared in source control so later `xcodebuild -scheme Pilot` is
executable on a clean checkout.

```bash
flutter pub get
flutter analyze
flutter test test/app/bootstrap_test.dart
flutter build apk --debug --flavor pilot -t lib/main_pilot.dart \
  --dart-define=PETAV3_API_BASE_URL=https://staging.example.invalid
./tool/verify_no_petav2.sh
./tool/verify_ios_schemes.sh
```

**Commit:** `chore(app): scaffold Flutter Android Pilot`

## Task 9 — Implement petaV3 authentication and API transport

**Repository:** Flutter client

**Files:**

- Create `lib/core/api/api_client.dart`, `api_failure.dart`, and
  `auth_interceptor.dart`
- Create `lib/features/auth/data/auth_api.dart` and `auth_repository.dart`
- Create `lib/features/auth/domain/auth_session.dart`
- Create `lib/features/auth/presentation/login_page.dart` and
  `login_view_model.dart`
- Create `lib/platform/secure_token_store.dart`
- Add focused tests under `test/core/api` and `test/features/auth`

**RED:** Test petaV3 token/refresh serialization, Keystore-backed store
contract, single-flight concurrent refresh, exactly one retry after 401,
refresh failure returning to login without deleting queued data, and redacted
logs.

```bash
flutter test test/core/api test/features/auth
```

**GREEN:** Implement `dio` transport and secure token facade. Derive all URLs
from the required petaV3 base URL. Never store JWT/password in SQLite and never
log authorization headers.

**Regression:**

```bash
flutter analyze
flutter test
./tool/verify_no_petav2.sh
```

**Commit:** `feat(auth): add petaV3 login and token refresh`

## Task 10 — Add the Drift ledger and account boundary

**Repository:** Flutter client

**Files:**

- Create `lib/core/database/app_database.dart` and generated Drift files
- Create `lib/features/interactions/data/interaction_dao.dart`
- Create `lib/features/interactions/data/interaction_mutation_dao.dart`
- Create `lib/features/uploads/data/upload_dao.dart`
- Create `lib/core/files/private_file_store.dart`
- Add migration/path/account tests under `test/core/database` and
  `test/core/files`

**RED:** Test schema creation/migration; one local active-capture interaction
while multiple queued/uploading interactions may coexist;
ordered create/transition/heartbeat mutation rows; transactional conditional
finalization; a unique upload per interaction/idempotency key; upload and
mutation atomic claim leases with expiry/takeover; pending-delete and startup
orphan cleanup; upload state and attempt count; account owner on every row;
queued-file preservation across logout; next-account isolation; and
canonical containment rejecting absolute paths, traversal, symlink escape,
and prefix-collision paths.

```bash
flutter test test/core/database test/core/files
```

**GREEN:** Implement the approved interaction/upload columns, bounded Lead
cache tables, durable ordered interaction mutation outbox, worker lease fields,
conditional state/finalizer updates, strict relative private paths, atomic
temp→fsync→rename publish, pending-delete cleanup, and transaction helpers.
Initialize the database before any worker.

**Regression:**

```bash
flutter analyze
flutter test test/core/database test/core/files
```

**Commit:** `feat(storage): add durable interaction and upload ledger`

## Task 11 — Add Lead search and Lead selection UI

**Repository:** Flutter client

**Files:**

- Create `lib/features/leads/domain/lead_summary.dart`
- Create `lib/features/leads/data/lead_api.dart` and `lead_repository.dart`
- Create `lib/features/leads/presentation/leads_page.dart`,
  `leads_view_model.dart`, `lead_detail_page.dart`
- Add tests under `test/features/leads`

**RED:** Test name/phone/email query encoding, opaque cursor handling,
typeahead cancellation, stale response suppression, bounded recent cache,
empty/error/loading states, Lead selection, and Phone/WhatsApp actions disabled
until an eligible phone exists.

```bash
flutter test test/features/leads
```

**GREEN:** Implement the Leads tab and detail view against only the new petaV3
endpoint. Do not download all Leads or add Lead editing/contact sync.

**Regression:**

```bash
flutter analyze
flutter test test/features/leads
```

**Commit:** `feat(leads): add petaV3 Lead search`

## Task 11A — Implement account-scoped badge binding in Flutter

**Repository:** Flutter client

**Files:**

- Create `lib/features/badge/data/badge_api.dart` and `badge_repository.dart`
- Create `lib/features/badge/domain/badge_binding.dart`
- Extend the account-scoped local database/DAO
- Add tests under `test/features/badge/data`

**RED:** Test server list/bind serialization, connected-device serial proof,
uppercase normalization, same-owner idempotency, owned-by-other conflict,
concurrent bind response, account isolation, and the distinction between local
disconnect and server ownership. A different signed-in account must never
inherit the previous account's active binding.

```bash
flutter test test/features/badge/data
```

**GREEN:** Implement the petaV3 badge endpoints and durable account-scoped
binding repository. The app may propose only the serial confirmed by the native
connected device. No first-release client transfer/unbind path exists.

**Regression:**

```bash
flutter analyze
flutter test test/features/badge/data test/features/auth
```

**Commit:** `feat(badge): add account-scoped badge binding`

## Task 11B — Synchronize the durable interaction outbox

**Repository:** Flutter client

**Files:**

- Create `lib/features/interactions/data/interaction_api.dart`
- Create `lib/features/interactions/data/interaction_repository.dart`
- Create `lib/features/interactions/application/interaction_sync_worker.dart`
- Add tests under `test/features/interactions/data`

**RED:** Test create/GET/PATCH/heartbeat serialization; create before later
mutations; acknowledged same UUID before releasing the BLE-start gate;
same-UUID immutable mismatch 409; different-active UUID 409 without local
rebind; ordered retry after process restart; mutation DB lease/expiry; PATCH
version/state conflict reconciliation; server stale-session response; and
late stop/upload inside the 24-hour recovery boundary; post-boundary audio
becoming unassigned with no silent event reactivation; newer-interaction
conflict; and recording upload remaining blocked until required interaction
mutations are acknowledged.

```bash
flutter test test/features/interactions/data
```

**GREEN:** Implement the API/repository and durable mutation executor. A normal
Lead-linked workflow cannot enter badge Start while create is pending/offline.
On an active conflict, surface the existing server UUID for recovery without
changing the local interaction's Lead/audio identity. Reconcile state with GET
after ambiguous network failures and replay mutations idempotently.

**Regression:**

```bash
flutter analyze
flutter test test/features/interactions/data test/core/database
```

**Commit:** `feat(sessions): sync interaction outbox`

## Task 12 — Implement the shared interaction state machine

**Repository:** Flutter client

**Files:**

- Create `lib/features/interactions/domain/interaction.dart`
- Create `lib/features/interactions/domain/interaction_state_machine.dart`
- Create `lib/features/interactions/application/interaction_coordinator.dart`
- Create platform contracts for badge control and external launch
- Add tests under `test/features/interactions/domain` and
  `test/features/interactions/application`

**RED:** Table-test all valid/invalid transitions, one-active invariant,
app/device start and stop combinations, ten-minute physical-start expiry,
same-UUID server create acknowledgement before BLE Start, offline blocking of a
normal matched start, confirmed recorder start before launch, launch failure
cancellation/stop, simultaneous stop idempotency through a conditional Drift
transaction, reconnect substate, process restart reconciliation, unexpected
offline/unselected start becoming unassigned, and foreground user-action
requirement after a background physical-start callback.

```bash
flutter test test/features/interactions
```

**GREEN:** Implement pure Dart models and coordinator with fake contracts. All
side effects must occur after durable local state writes. A background callback
may confirm recording but cannot autonomously open Phone/WhatsApp. A queued
server create is not permission to start; the exact UUID must be acknowledged.

**Regression:**

```bash
flutter analyze
flutter test test/features/interactions
```

**Commit:** `feat(sessions): add interaction state machine`

## Task 13 — Port the pure YHY02 protocol and AAC validation

**Repository:** Flutter client

**Files:**

- Create `packages/yhy02_badge/pubspec.yaml`
- Create Dart protocol files under `packages/yhy02_badge/lib/src/protocol`
- Create ADTS/files helpers under `packages/yhy02_badge/lib/src/audio`
- Copy only approved sanitized golden byte fixtures into
  `packages/yhy02_badge/test/fixtures`
- Add focused package tests

**RED:** Test real validated frames for envelope length, direction, command,
sequence, status, `0x0022` BE32 timestamp/timezone payload, ACK decoding, FEE4
ordering, ADTS header/frame lengths, sequence gaps, corrupt/truncated frames,
and monotonic elapsed calibration. Add negative dispatcher tests proving
`0x0043`, `0x0032`, `0x0030`, OTA command IDs, and an arbitrary unknown command
are rejected before any transport write.

```bash
(cd packages/yhy02_badge && flutter pub get && flutter test)
```

**GREEN:** Port minimal codec/state parsing behavior from the approved Kotlin
reference and protocol facts. Use an explicit production command allow-list.
Do not define or dispatch destructive operations in the public plugin API, and
keep an internal deny-by-default dispatcher even if a raw command is presented.

**Regression:**

```bash
(cd packages/yhy02_badge && flutter analyze && flutter test)
```

**Commit:** `feat(badge): add YHY02 protocol and AAC codec`

## Task 14 — Implement the Android BLE transport and typed bridge

**Repository:** Flutter client

**Files:**

- Add Pigeon definitions under `packages/yhy02_badge/pigeons`
- Add Android Kotlin plugin files under
  `packages/yhy02_badge/android/src/main/kotlin/...`
- Add BLE manifest permissions for Pilot and plugin
- Add Kotlin unit tests under `packages/yhy02_badge/android/src/test`
- Add Dart contract tests under `packages/yhy02_badge/test`

**RED:** Test scan filtering, permission matrix by Android version, serial
normalization, connect/disconnect, MTU negotiation, service/characteristic
discovery, CCCD readiness ordering, serialized GATT operation queue,
connection-generation isolation, stale callback rejection, tail-frame FIFO
before disconnect, native↔Dart event mapping, and a Kotlin-layer deny test that
forbidden/unknown commands perform zero `BluetoothGatt.writeCharacteristic`
calls.

```bash
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest)
(cd packages/yhy02_badge && flutter pub get && flutter test)
```

**GREEN:** Implement the smallest Kotlin BLE adapter behind typed Pigeon APIs.
Expose device info, connection, recording state, and audio events; no UI logic
or process-lifecycle ownership in this commit.

**Regression:**

```bash
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest)
(cd packages/yhy02_badge && flutter analyze && flutter test)
flutter build apk --debug --flavor pilot -t lib/main_pilot.dart \
  --dart-define=PETAV3_API_BASE_URL=https://staging.example.invalid
```

**Commit:** `feat(badge): add Android BLE transport`

## Task 14A — Make the Android foreground service own active capture recovery

**Repository:** Flutter client

**Files:**

- Create the connected-device foreground service and notification channel
- Create a minimal native session journal and AAC temp writer
- Add manifest foreground-service declarations and Android backup exclusions
- Add pure Kotlin, Robolectric, and Android instrumentation tests
- Add Dart startup reconciliation tests

**RED:** Separate tests must cover foreground-service start/stop ownership,
persistent recording notification, native journal fsync/reopen, FEE4 AAC writes
without an Activity/cached-engine dependency, the native service remaining the
only writer across foreground→background→foreground with no duplicate/missing
tail frames or file-descriptor handoff, generation isolation, callback handoff
when Dart is alive, journal→Drift reconciliation, OS-kill restart where
the platform permits it, and user force-stop remaining unrecoverable until the
next explicit launch/history query. No JVM fake is labeled as proof of the
force-stop behavior.

```bash
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest testPilotDebugUnitTest)
(cd android && ./gradlew connectedPilotDebugAndroidTest)
flutter test test/features/recordings/native_journal_reconciliation_test.dart
```

**GREEN:** The native service is the only AAC writer for the entire active
session, not merely while Dart/Activity is absent. Flutter receives progress,
gap, and finalized-file/journal events but never opens the active file for
writing. On bootstrap, reconcile the journal idempotently into Drift. Document
ordinary background, OS kill, and user force-stop as distinct outcomes; after
force-stop, recover from badge history only when the user reopens the app.

**Regression:** Same three layers plus a Pilot debug build.

**Commit:** `feat(badge): persist Android capture across lifecycle`

## Task 15 — Validate and enable `0x0022` recorder control

**Repository:** Flutter client

**Files:**

- Modify Android plugin control path and Dart badge facade
- Add/extend protocol and adapter tests
- Create `docs/validation/YHY02-0022-ANDROID-PILOT.md`

**RED:** Add fake/native tests for Start/Stop command bytes, ACK timeout,
nonzero result, state-confirmation timeout, duplicate/racing commands,
disconnect during command, and no external launch before ACK plus recording
notification.

```bash
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest)
(cd packages/yhy02_badge && flutter test)
flutter test test/features/interactions
```

**GREEN:** Implement only FEE3 `0x0022` Start/Stop using protocol-authoritative
payload/ACK parsing.

**Mandatory human gate:** Before any real-device write, print this checklist and
wait for confirmation:

```text
Phone has Flutter Pilot foreground; old Kotlin badge service is stopped.
Badge is YHY02_5BA8E1 / SN C8478C5BA8E1 and has sufficient battery/storage.
Human is ready to observe LED and speak for each bounded recording.
Allowed write: FEE3 0x0022 Start/Stop only.
Forbidden: 0x0043, 0x0032, 0x0030, OTA, all undocumented writes.
```

Then validate app Start→ACK→status→valid AAC; app Stop→ACK→closed AAC; app
Start+physical Stop; physical Start+app Stop; repeated control; disconnect and
recovery; and timestamp behavior. Use `ffprobe` on closed files and record
observed firmware/device info. Stop immediately on protocol contradiction.

**Regression:** Same automated tests plus the signed validation record.

**Commit:** `feat(badge): enable validated recorder control`

## Task 16 — Finalize live AAC capture into the Flutter ledger

**Repository:** Flutter client

**Files:**

- Create/modify badge live-audio APIs in the internal plugin
- Create `lib/features/recordings/application/recording_capture_coordinator.dart`
- Add Dart/Kotlin tests and fixtures

**RED:** Test native ordered FEE4 evidence, one atomic finalized file handed to
Dart, Flutter never opening the active temp file for write, foreground/
background transitions remaining one file, stop-race finalization, sequence gap
marking live audio suspect, storage error, disconnect/reconnect,
native-journal reconciliation, process restoration, conditional one-time
finalization/upload enqueue, and no badge deletion command.

```bash
flutter test test/features/recordings packages/yhy02_badge/test
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest)
```

**GREEN:** Coordinate the native single-writer capture and reconcile its
atomically finalized journal/file into exactly one Drift upload row. Dart never
streams the active AAC into a second file. Mark sequence-gap files suspect for
the next history-recovery task.

**Regression:**

```bash
flutter analyze
flutter test test/features/recordings packages/yhy02_badge/test
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest)
```

**Commit:** `feat(recordings): capture live badge AAC`

## Task 16A — Recover only targeted badge-history recordings

**Repository:** Flutter client

**Files:**

- Add targeted history APIs to the internal plugin
- Create `lib/features/recordings/application/history_recovery.dart`
- Add Dart/Kotlin history fixtures and tests

**RED:** Test expected time/file bounds, unique candidate recovery, hash/file
comparison with a suspect live capture, ambiguous candidates becoming
unassigned, no candidate, interrupted download resume/retry semantics, account
and badge isolation, next-launch recovery after force-stop, and proof that no
history path issues delete/format/hotspot/OTA commands.

```bash
flutter test test/features/recordings/history_recovery_test.dart
(cd android && ./gradlew :yhy02_badge:testDebugUnitTest)
```

**GREEN:** Treat badge history as recovery authority but fetch only the active
or explicit unassigned candidate range. Never bulk-associate arbitrary files
to the last Lead. Reconciliation is idempotent and retains the badge copy.

**Regression:** Run recording, protocol, and Android plugin suites.

**Commit:** `feat(recordings): add targeted badge history recovery`

## Task 17 — Build Phone/WhatsApp and Active Session experience

**Repository:** Flutter client

**Files:**

- Create `lib/platform/external_call_launcher.dart`
- Create `lib/features/interactions/presentation/start_interaction_page.dart`
- Create `active_session_page.dart` and corresponding view models
- Wire Lead detail actions and app lifecycle restoration
- Add widget/coordinator tests

**RED:** Test E.164/WhatsApp digit normalization, `tel:` launch, approved
`https://wa.me/<digits>` launch, no `+`/spaces/hyphens in WhatsApp URL, start
choice UI, badge-readiness block, physical-start waiting foreground rule,
monotonic timer, return from external app, physical stop auto-finalization,
prominent End and upload, and launch failure cancellation.

```bash
flutter test test/features/interactions test/platform
```

**GREEN:** Implement the two approved launch paths. WhatsApp opens chat only;
the user taps WhatsApp voice call. Do not read CallLog, contacts, WhatsApp
notifications, or claim that a call was answered.

**Regression:**

```bash
flutter analyze
flutter test
```

**Commit:** `feat(calls): add recorded Phone and WhatsApp flow`

## Task 18 — Add resilient automatic upload

**Repository:** Flutter client

**Files:**

- Create `lib/features/uploads/data/recording_upload_api.dart`
- Create `lib/features/uploads/application/upload_coordinator.dart`
- Create Android WorkManager entrypoint/initializer
- Add API-level conditional foreground-service permissions, manifest merge for
  WorkManager `SystemForegroundService` with `dataSync` type, notification UI,
  upload tests, Robolectric/manifest tests, and device instrumentation tests

**RED:** Test streamed multipart fields/hash, interaction-create/update outbox
ordering before its recording upload, immediate enqueue, Wi-Fi and mobile
eligibility, offline retry, bounded exponential backoff+jitter, one
refresh on 401, retained file on failed refresh/4xx/5xx, visible permanent
failure, duplicate 2xx, filtered 2xx, server ID committed before deletion,
strict containment before read/delete, atomic DB claim lease, lease expiry and
takeover after crash, foreground/UI executor racing WorkManager, unique work
`KEEP`, background-isolate plugin/bootstrap initialization, long upload in a
foreground Worker, interruption/retry from the beginning without duplicate
server rows, API 34+ foreground-service permission/type without
`SecurityException`, visible data-sync notification, process restart, and one
worker per idempotency key.

```bash
flutter test test/features/uploads
(cd android && ./gradlew testPilotDebugUnitTest)
(cd android && ./gradlew connectedPilotDebugAndroidTest)
```

**GREEN:** Initialize Drift before WorkManager, stream the AAC from disk, and
atomically claim each upload with a bounded DB lease before streaming from
disk. Classify responses exactly per contract. Long work uses an Android
foreground Worker; retries restart the idempotent multipart upload rather than
pretending to resume bytes. On durable stored/duplicate/filtered 2xx,
transactionally record the server outcome plus pending-delete and only then
delete the phone file. Startup cleanup completes interrupted deletions. Never
request badge deletion. Android 14+ manifests include `FOREGROUND_SERVICE`,
`FOREGROUND_SERVICE_DATA_SYNC`, and the merged `dataSync` service type only as
required by supported SDK behavior.

**Regression:**

```bash
flutter analyze
flutter test
(cd android && ./gradlew testPilotDebugUnitTest)
(cd android && ./gradlew connectedPilotDebugAndroidTest)
```

**Commit:** `feat(upload): auto-upload recordings to petaV3`

## Task 19 — Complete Sessions and unassigned assignment

**Repository:** Flutter client

**Files:**

- Create Sessions page/view model
- Create explicit unassigned-recording Lead assignment flow
- Add focused widget/domain tests

**RED:** Test active/queued/uploading/matched/filtered/failed/unassigned groups,
retry eligibility, explicit Lead assignment, server-create ACK before an
unassigned upload, active-conflict handling without rebind, and account/badge
isolation.

```bash
flutter test test/features/sessions
```

**GREEN:** Implement the approved session groups, retry affordances, and
explicit unassigned→Lead workflow. Never guess a Lead from recency.

**Regression:**

```bash
flutter analyze
flutter test
flutter build apk --debug --flavor pilot -t lib/main_pilot.dart \
  --dart-define=PETAV3_API_BASE_URL=https://staging.example.invalid
```

**Commit:** `feat(sessions): add session recovery UI`

## Task 19A — Complete Badge, Settings, logout, and account isolation

**Repository:** Flutter client

**Files:** Badge and Settings pages/view models, logout coordinator, and tests

**RED:** Test scan/bind/connect/disconnect, device information, safe controls,
absence of delete/format/hotspot/OTA, badge-owner conflict, permission/network
diagnostics, queued data preservation on logout, native service shutdown, and
no previous-account Lead/badge/session exposure after the next login.

```bash
flutter test test/features/badge test/features/settings test/features/auth
```

**GREEN:** Implement the approved pages and cancellation-safe logout. Local
disconnect does not unbind server ownership. Queued data stays account-scoped.

**Regression:** `flutter analyze && flutter test`

**Commit:** `feat(app): add Badge and Settings account controls`

## Task 19B — Produce reproducible Pilot and Production branding

**Repository:** Flutter client

**Files:** 1024×1024 master, Android adaptive assets, opaque iOS assets, Pilot
variant, icon generation config, and build-identity tests

**RED:** Add asset/build tests for required sizes, no iOS alpha, Android safe
zone, distinct Pilot marker, labels, and package IDs.

**GREEN:** Redraw the provided house logo faithfully rather than scaling the
150×150 bitmap, then generate reproducible assets. Use the image editing skill
and retain the master/config in the repository.

**Regression:** Build Pilot debug and run asset tests.

**Commit:** `feat(brand): add PropertyLab Sales Agent icons`

## Task 20 — Run Android Pilot end-to-end acceptance

**Repositories:** Flutter client plus deployed petaV3 staging; no unreviewed
server code in this task

**Files:**

- Create `integration_test/android_pilot_workflow_test.dart`
- Create `docs/validation/ANDROID-PILOT-ACCEPTANCE.md`
- Fix only defects discovered by the acceptance matrix, with a failing
  regression test per defect and separate atomic fix commits

**RED:** Add the integration journey and validation document assertions first;
observe failure because the new test/required evidence is absent or incomplete.
Any product defect found during the matrix receives its own failing regression
before a fix.

**Automated gate:**

```bash
flutter analyze
flutter test
(cd android && ./gradlew testPilotDebugUnitTest lintPilotDebug)
flutter build apk --release --flavor pilot -t lib/main_pilot.dart \
  --dart-define=PETAV3_API_BASE_URL=<approved-staging-url>
flutter test integration_test/android_pilot_workflow_test.dart -d <device-id>
./tool/verify_no_petav2.sh
```

Flutter integration tests cannot operate Phone/WhatsApp system UI or press the
physical badge. Print the human matrix and wait before manual execution:

| Channel | Start | Stop |
|---|---|---|
| Phone | App | App |
| Phone | App | Badge |
| Phone | Badge | App |
| Phone | Badge | Badge |
| WhatsApp | App | App |
| WhatsApp | App | Badge |
| WhatsApp | Badge | App |
| WhatsApp | Badge | Badge |

Also verify disconnect/history recovery; ordinary background; recoverable OS
kill; user force-stop followed by explicit reopen/history recovery; Wi-Fi,
mobile data; offline-after-create retry; blocked normal start when offline
before create ACK; long-file memory behavior; duplicate retry; exact
petaV3 Lead linkage, local deletion after durable acknowledgement, and badge
retention. Record device, app commit, server commit, timestamps, and server IDs.

**GREEN:** The automated journey and every manual matrix row have recorded
evidence with no unresolved blocker. Before using a real customer's call, stop
until the business/privacy owner approves recording notice, consent, retention,
access, and deletion policy for the Pilot jurisdiction. Synthetic/test calls
may validate the mechanics first.

**Commit:** `test(app): certify Android Pilot workflow`

## Task 21 — Prepare Android Production replacement

**Repository:** Flutter client

**Files:**

- Create `docs/release/ANDROID-PRODUCTION-CUTOVER.md`
- Add release/signing/package verification scripts or tests
- Add an in-place upgrade test harness and legacy-data inventory
- Make only reviewed production-flavor adjustments

**RED:** Add a build verification that Pilot remains separately installable and
Production resolves to `tech.propertylab.agent`, expected signing identity,
approved petaV3 URL, and no petaV2 string. Add a cutover check that blocks while
the Kotlin app has unaudited pending uploads. Install each supported Kotlin APK
with seeded legacy WorkManager/database/preferences/files, then attempt the
Flutter Production update and observe the failing inventory/migration tests.

```bash
flutter test test/release
(cd android && ./gradlew testProductionDebugUnitTest lintProductionDebug)
./tool/verify_android_upgrade.sh --from <supported-kotlin-apk> \
  --to build/app/outputs/flutter-apk/app-production-release.apk
```

**GREEN:** Document bounded cohort, staged rollout, zero-pending-or-migration
decision, old WorkManager job cancellation/class compatibility, old database/
preferences/file migration or audited retirement, old-service shutdown,
monotonic `versionCode`, rollback APK/data compatibility, server backward
compatibility, monitoring, and rollback thresholds. Require explicit
business/privacy approval for notice, consent, retention, access, and deletion
in every release jurisdiction. Require evidence that the recording-link unique
index is verified or that a current named owner has granted a time-bounded
Production defer with deadline/risk; an expired Pilot defer blocks release. Do
not overwrite signing material or ship without owner confirmation.

**Regression:** Run release tests, Production lint/build/signing verification,
and every supported in-place upgrade case. A fresh install is insufficient.

**Commit:** `docs(app): add Android production cutover gate`

---

## iOS follow-on — scheduled after Android stability

## Task 22 — Implement the Swift Core Bluetooth adapter

**Files:** Swift plugin implementation, restoration journal, iOS entitlements/
privacy strings, native tests, and shared contract fixtures.

**RED:** Run the shared golden workflow against a fake Swift transport and add
native tests for scan/connect/CCCD ordering, generation isolation, restoration,
deny-by-default command dispatch, and physical/app control callbacks.

```bash
(cd ios && xcodebuild test -workspace Runner.xcworkspace -scheme Pilot \
  -destination 'platform=iOS Simulator,name=iPhone 16')
(cd packages/yhy02_badge && flutter test)
```

**GREEN:** Implement the same typed `BadgeController` contract with Core
Bluetooth restoration/background modes. User force-quit remains a documented
next-launch history-recovery case, not a relaunch promise.

**Regression:** Swift native tests, shared Dart contract suite, and iOS build.

**Commit:** `feat(badge): add iOS Core Bluetooth adapter`

## Task 23 — Complete iOS platform services and parity UI

**Files:** Keychain store, native background `URLSession` bridge/task journal,
backup exclusions, external launcher configuration, and tests.

**RED:** Test ThisDeviceOnly Keychain accessibility, AAC/SQLite backup
exclusion, persisted background URLSession task IDs, relaunch delegate
reconciliation, suspension/OS termination, user force-quit limitation,
`tel:`/`wa.me`, and shared state-machine parity.

**GREEN:** Add iOS services without forking shared business logic. Long upload
uses native background `URLSession`; generic BGTask is not treated as a timing
guarantee. Reconcile completed tasks idempotently on relaunch.

**Regression:** Dart, Swift, and iOS integration suites.

**Commit:** `feat(ios): add platform services for app parity`

## Task 24 — Certify iOS parity and TestFlight build

**RED:** Add the iOS parity integration journey and evidence checklist first;
observe failure until all automated and real-device evidence exists.

**GREEN:** Run the same eight start/stop/channel combinations and recovery/
network matrix on a real iPhone. Distinguish suspension, OS termination, and
user force-quit; the latter recovers on next launch. Verify background
URLSession completion/reconciliation, App Store privacy/recording disclosures,
backup exclusions, and a TestFlight archive.

**Regression:** `flutter analyze`, all Dart tests, Xcode tests, integration
journey, archive validation, and signed manual matrix.

**Commit:** `test(ios): certify PropertyLab app parity`

## Android completion report

At Task 21 completion, report every task with:

- `DONE`, `DONE_WITH_CONCERNS`, or `BLOCKED`;
- targeted RED and GREEN test results;
- relevant regression results;
- commit SHA in its repository;
- petaV3 `dev-chen` cherry-pick SHA when applicable;
- real-device result and artifact paths;
- unresolved risks and production blockers; and
- recommendation for the petaV3 staging integration/pilot cohort.

Do not call Android complete merely because it builds. Completion requires the
full deterministic server link and the real-device acceptance matrix.
