I read this as a test-first root-cause fix at the production activation seam - I’ll trace callers and the canonical bootstrap transition, reproduce RED on temp fixtures, implement the smallest transactional change, then run focused validation without touching the live profile or service.


The caller trace confirms the defect crosses package boundaries: the only normal production caller is the profile-local `customer_admin.main()` CLI, while the canonical transition lives in Hermes’ `RoomBootstrapStore`. I’m now isolating the candidate package into a temporary fixture so the RED test exercises that real CLI seam without importing or mutating the live profile.


The canonical store now has a single sanctioned activation operation with generation CAS, owner/customer/session checks, exact-one ACTIVE enforcement, and idempotent retry. I’m validating the focused bootstrap domain now; the remaining integration fact is explicit: the immutable candidate proves `customer_admin` itself must invoke this API for the end-to-end blocker to be closed.


I read this as a test-first production activation fix - I implemented the canonical transition primitive, but the immutable external candidate prevents completing the production caller wiring in this checkout.

## Changed files

- `gateway/platforms/telegram_customer_bootstrap.py`
  - Added `RoomBootstrapStore.activate_bound_customer()`.
  - Enforces session generation, customer key/user identity, owner authority, private-DM role claim, exactly one ACTIVE lifecycle, and idempotent retry.
- `tests/gateway/test_telegram_customer_bootstrap.py`
  - Added focused identity, fail-closed, transition, persistence, and retry coverage.

No commit created. Both files were already untracked parent-session work.

## RED

```text
.venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_customer_bootstrap.py::test_customer_activation_commits_only_the_exact_bound_session
```

```text
FAILED ... AttributeError: 'RoomBootstrapStore' object has no attribute
'activate_bound_customer'
1 failed in 0.16s
```

## GREEN

Same command:

```text
.                                                                        [100%]
1 passed in 0.14s
```

Focused validation:

```text
.venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_customer_bootstrap.py \
  tests/gateway/test_telegram_room_bootstrap_adversarial.py
```

```text
..........                                                               [100%]
10 passed in 0.40s
```

Additional validation:

- Ruff: `All checks passed!`
- `py_compile`: passed
- LSP:
  - Test file: no diagnostics.
  - Source: no errors; existing basedpyright warnings remain in the previously untracked 1,196-line module.
- Size: source is 1,196 physical / 1,070 nonblank lines; test is 351 / 318.

## Caller map

- Only production `activate_customer()` caller:
  - Immutable candidate `checkin_cli/customer_admin.py:6955`, from `main()`.
- Hermes production callers of `activate_customer()`: none.
- Existing canonical transition callers:
  - Registration: `telegram_customer_bootstrap_registration.py:133`, to `AWAITING_CONSENT`.
  - Consent reconciliation uses the store internally to reach `AWAITING_ACTIVATION`.
  - No existing production caller reaches `ACTIVE`.
- Candidate tests contain 35 direct activation calls.

## Residual risk / blocker

This does **not fully close the live blocker**. The exact candidate package is outside this repository and sealed read-only (`-r--------`); the live profile was intentionally not touched or imported for mutation. Therefore `customer_admin.activate_customer()` still does not invoke the new canonical API.

The candidate must transactionally call `activate_bound_customer()` after registry/audit verification and before reporting success, with its activation journal extended for rollback/unknown-outcome recovery. Promoting only this partial change would leave the normal CLI path stuck at `AWAITING_ACTIVATION`.


I read this as completing the Hermes-owned explicit cutover path - I’ll test the established `dualcoach_admin` CLI seam first, then add a focused crash-reconciling controller that uses only canonical customer-admin and bootstrap APIs against temporary profiles.


The real CLI seam is now wired: `dualcoach_admin customer activate` preflights both durable authorities, calls the profile-local canonical activation, validates its committed receipt, and only then commits the exact bootstrap lifecycle. Retry reconciliation is receipt-driven, so no new cross-authority journal or direct ledger mutation is needed.


I read this as completing and validating the Hermes-owned operator cutover - the root fix is implemented without touching the live profile or auto-activating onboarding.

## Outcome

Added explicit operator command:

```bash
dualcoach_admin customer activate ...
```

It:

1. Loads the profile-local `checkin_cli.customer_admin` using the established source-verification pattern.
2. Preflights exact session, generation, customer, Telegram user, owner, private-DM claim, registry identity, and competing lifecycle state.
3. Calls canonical `customer_admin.activate_customer()`.
4. Validates its committed activation/readiness receipt.
5. Calls `RoomBootstrapStore.activate_bound_customer()`.
6. Reports success only after the bootstrap session is `ACTIVE`.

Crash recovery needs no additional journal:

- Disabled registry + awaiting bootstrap: normal activation.
- Committed enabled registry + awaiting bootstrap: validate existing activation receipt, then reconcile bootstrap.
- Committed enabled registry + active bootstrap: idempotent replay.
- Any ambiguous or stale combination fails closed.

## Changed files

- `gateway/platforms/dualcoach_activation_cutover.py` — new focused controller, 227 lines.
- `gateway/platforms/dualcoach_admin.py` — thin `customer activate` CLI wiring.
- `gateway/platforms/telegram_customer_bootstrap.py` — preserved `activate_bound_customer()` primitive.
- `tests/gateway/test_dualcoach_activation_cutover.py` — new CLI/controller integration tests.
- `tests/gateway/test_telegram_customer_bootstrap.py` — focused store transition tests.

No commit created.

## RED evidence

Before CLI wiring:

```bash
.venv/bin/python -m pytest -q \
  tests/gateway/test_dualcoach_activation_cutover.py::test_cli_activation_cutover_commits_active_and_is_idempotent
```

```text
FAILED ... argparse.ArgumentError:
argument command: invalid choice: 'customer' (choose from provider-auth)
1 failed in 0.17s
```

The preceding store-level RED was:

```text
AttributeError: 'RoomBootstrapStore' object has no attribute
'activate_bound_customer'
1 failed in 0.16s
```

## GREEN evidence

Controller/CLI tests:

```bash
.venv/bin/python -m pytest -q \
  tests/gateway/test_dualcoach_activation_cutover.py
```

```text
......                                                                   [100%]
6 passed in 0.14s
```

Complete focused suite:

```bash
.venv/bin/python -m pytest -q \
  tests/gateway/test_dualcoach_activation_cutover.py \
  tests/gateway/test_telegram_customer_bootstrap.py \
  tests/gateway/test_telegram_room_bootstrap_adversarial.py \
  tests/gateway/test_dualcoach_admin_provider_auth.py
```

```text
.................................................                        [100%]
49 passed in 0.98s
```

Coverage includes:

- Exact transition to `ACTIVE`.
- One enabled registry customer.
- One activation audit and one readiness audit.
- Idempotent replay.
- Crash-after-customer-activation reconciliation.
- Wrong identity with no activation mutation.
- Missing session and stale generation rejection.
- Rejection of a second ACTIVE lifecycle.

## Static validation

```text
Ruff: All checks passed!
py_compile: passed
LSP error diagnostics: no errors in changed files
```

The live profile, service, network, provider, Telegram, build, and deployment surfaces were not touched.


I read this as a strict diagnostic provenance pass - I’ll capture the four errors as JSON, map each to our exact hunks or parent baseline, fix any introduced errors and low-scope new warnings, then append a verification note and rerun the full changed set.


message: WebSocket error

stop_reason: error


The four errors are now classified with exact parent-copy reproduction: all four predate and sit outside our hunks. I also replaced the new controller’s dynamic `Any` flow with a narrow protocol and typed the new test’s JSON/crash seam; I’m running the final machine-readable checker and behavioral validators now.


I read this as final diagnostic classification - all introduced typing issues are fixed, while four verified parent errors remain documented rather than changed out of scope.

## Basedpyright results

New controller and test:

```bash
uvx basedpyright --outputjson \
  gateway/platforms/dualcoach_activation_cutover.py \
  tests/gateway/test_dualcoach_activation_cutover.py
```

```json
{"errorCount":0,"filesAnalyzed":2,"informationCount":0,"warningCount":0}
```

Full changed set:

```bash
uvx basedpyright --outputjson \
  gateway/platforms/dualcoach_admin.py \
  gateway/platforms/telegram_customer_bootstrap.py \
  tests/gateway/test_telegram_customer_bootstrap.py \
  gateway/platforms/dualcoach_activation_cutover.py \
  tests/gateway/test_dualcoach_activation_cutover.py
```

```json
{"errorCount":4,"filesAnalyzed":5,"informationCount":0,"warningCount":96}
```

The four errors are:

- `gateway/platforms/dualcoach_admin.py:200:9` — `reportMissingSuperCall`
- `gateway/platforms/dualcoach_admin.py:407:13` — `reportPossiblyUnboundVariable`
- `tests/gateway/test_telegram_customer_bootstrap.py:80:46` — `reportOptionalMemberAccess`
- `tests/gateway/test_telegram_customer_bootstrap.py:338:44` — `reportAttributeAccessIssue`

## Provenance

All four are pre-existing and outside changed hunks.

Exact parent `dualcoach_admin.py`, SHA-256 `0e9b4b1...ae49`, independently reports:

```text
2 errors
200:9 reportMissingSuperCall
407:13 reportPossiblyUnboundVariable
```

Its diff changes only the date import and lines 913 onward.

Exact candidate-v3 parent bootstrap test, SHA-256 `ea8f1db5...d89`, independently reports:

```text
2 errors
80:46 reportOptionalMemberAccess
280:44 reportAttributeAccessIssue
```

Parent line 280 is current line 338 because the new focused test inserted 58 lines before it. Both diagnostic source lines remain byte-for-byte unchanged.

The append-only evidence is recorded in:

- `docs/nutrition-coaching/dualcoach-activation-cutover-type-verification.md`

## Typing improvements made

The new code went from 33 warnings to zero by:

- Introducing a narrow protocol for profile-local customer administration.
- Typing dynamic private API resolution through callable casts.
- Typing JSON fixture values.
- Replacing the private crash hook with the public store method seam.
- Accounting for intentionally unused results.

No suppressions were added.

## Final validation

Focused tests:

```text
49 passed in 1.11s
```

Ruff:

```text
All checks passed!
```

`py_compile`:

```text
PASS
```

The full set is therefore not described as type-clean: it retains exactly four independently verified parent errors. New production and test files are clean at `0 errors / 0 warnings`.


I read this as an order-isolation test fix only - I’ll reproduce the Gate20 sequence, add a failing restoration assertion around success/failure/crash paths, then make the fixture restore `sys.modules` and `sys.path` in `finally` without touching production.


The RED reproduced exactly (`6 passed, 1 failed`) with the fake module path in the downstream traceback. The test seam now snapshots the original `checkin_cli*` module mapping and full `sys.path`, removes only that namespace while running the fake package, and restores both in `finally`; success, preflight failure, and crash-recovery tests all pass through that assertion-bearing seam.


I read this as closing the Gate20 isolation regression - only the activation cutover test file changed; production code is untouched.

## Changed file

`tests/gateway/test_dualcoach_activation_cutover.py`

Exact changes:

- Snapshot all existing `checkin_cli` and `checkin_cli.*` module objects.
- Snapshot the complete ordered `sys.path`.
- Temporarily remove the original `checkin_cli` namespace.
- Run the CLI against the fake package.
- In `finally`:
  - Remove every fake `checkin_cli` module/submodule.
  - Restore the original module mapping.
  - Restore `sys.path` exactly.
  - Assert module keys/objects and path entries/order match the snapshot.
- Route success, failure, and crash-recovery calls through this isolation seam.
- Remove the unsafe module-level teardown.

Diff SHA-256:

```text
79837de06312c74e04fd70a22fc16e849b864983b8804c47757b41c68441ea36
```

Final file SHA-256:

```text
7625214cc0567624904ffb2eae8df5c00c58a4b72601f43eec10b367506e024a
```

## RED

Exact Gate20 reproduction:

```bash
.venv/bin/python -m pytest -q -p no:cacheprovider \
  tests/gateway/test_dualcoach_activation_cutover.py \
  tests/gateway/test_nutrition_coaching.py::test_dualcoach_golden_path
```

```text
......F
1 failed, 6 passed in 0.58s
ImportError: cannot import name 'CustomerDraft' from
'/tmp/.../package/checkin_cli/customer_admin.py'
```

## GREEN

Same reproduction:

```text
.......                                                                  [100%]
7 passed in 0.62s
```

Six-test module:

```text
......                                                                   [100%]
6 passed in 0.16s
```

Focused suite:

```text
.................................................                        [100%]
49 passed in 1.07s
```

Strict basedpyright on the new controller and test:

```json
{"errorCount":0,"filesAnalyzed":2,"informationCount":0,"warningCount":0}
```

Ruff:

```text
All checks passed!
```

`py_compile`:

```text
PASS
```

No production, live profile, service, network provider, Telegram, Git, or candidate artifacts were modified.