I read this as an advisory architecture-and-execution planning task - I’ll inspect the full NutriCoach transaction, preflight, test, migration, and deployment surfaces in an isolated worktree, compare viable designs, then return a decision-complete RED-to-GREEN wave plan without editing product code or touching production.


I read this as advisory-only completion of the v1.1.1 plan - I have enough repository evidence, so I’m closing the clean planning worktree and will provide the ordered plan, gates, and evidence without changing product or production state.


WebSocket error


I read this as advisory-only completion of the v1.1.1 stability plan - below is the recommended architecture, ordered RED-to-GREEN implementation sequence, qualification evidence, migration/rollback procedure, and cleanup gates.

# NutriCoach v1.1.1 stability plan

## 1. Recommendation

Use an incremental recovery saga built from the authorities already present:

- Bootstrap CAS ledger owns authenticated consent handoff.
- Customer registry remains consent authority.
- Nutrition onboarding store remains workflow authority.
- Publication outbox remains message-attempt authority.
- Polling receipt store remains Telegram ingress authority.
- Task26 production authority remains package/candidate authority.

Do not introduce a unified transaction journal.

| Design | Advantages | Problems | Decision |
|---|---|---|---|
| Existing authorities plus durable saga/reconciliation | Small change set, preserves proven formats, isolated rollback, supports incremental RED/GREEN | Requires explicit cross-authority reconciliation | Recommended |
| New unified journal/coordinator | One apparent transaction authority | Migrates every state machine, duplicates existing ledgers, broad regression and rollback risk | Reject |
| Delete/re-import `checkin_cli` from `sys.modules` | Can appear to repair wrong import source | Produces split class identities and unsafe process-global state | Reject |
| Isolated profile subprocess/RPC | Strong package isolation | Large operational surface and unnecessary IPC | Reject for v1.1.1 |

### Telegram delivery limitation

Telegram `sendMessage` has no idempotency key and no API for finding a sent message after losing its response. Therefore a hard crash after Telegram accepted an unanchored send but before any local receipt cannot honestly guarantee both:

1. retry to eventual delivery, and
2. no duplicate.

The correct contract is:

- Known-message `editMessageText`: retry safely; repeated identical edit is treated as success when Telegram returns `message is not modified`.
- Unanchored `sendMessage`: persist `DISPATCHING` before the call; after ambiguous restart, transition to durable `UNKNOWN` and never resend automatically.
- Business-commit-before-publication crashes: recover and publish once with a normal durable receipt.

Tests must assert this boundary rather than fabricating a provider receipt.

---

## 2. Isolation and immutable baseline

Use a new implementation worktree; never implement in the deployed checkout.

```bash
set -euo pipefail
umask 077

BASE=ccd2eee8a97a13d25c36c1446ed5b5aca2e88653
SRC=/home/cube/projects/richard/hermes-agent-dualcoach-v1.1
WT=/home/cube/projects/richard/.worktrees/nutricoach-v111-impl
EV="$WT/.omo/evidence/v1.1.1-stability"

test -z "$(git -C "$SRC" status --porcelain)"
test ! -e "$WT"
git -C "$SRC" worktree add --detach "$WT" "$BASE"

cd "$WT"
mkdir -p "$EV"/{red,green,baseline,regression,typecheck,build,migration,production-seal}
uv venv --python 3.12 .venv
uv sync --frozen --extra dev --extra messaging
uv pip install --python .venv/bin/python --no-deps -e dualcoach/profile

git rev-parse HEAD | tee "$EV/base-commit.txt"
git status --porcelain=v1 | tee "$EV/initial-status.txt"
test "$(git rev-parse HEAD)" = "$BASE"
test ! -s "$EV/initial-status.txt"
```

Before any production source edit, capture a read-only production seal:

- Service: `hermes-gateway-dualcoachtest.service`
- Required current `MainPID`: `3771761`
- Hash, without printing values:
  - systemd unit and drop-ins;
  - deployed executable and installed distributions;
  - `config.yaml` and `.env`;
  - Task26 candidate authority;
  - registry;
  - bootstrap ledger;
  - onboarding sessions;
  - publication outboxes;
  - ingress receipt ledger;
  - activation and membership journals.

```bash
test "$(systemctl --user show \
  hermes-gateway-dualcoachtest.service \
  --property MainPID --value)" = 3771761

systemctl --user show hermes-gateway-dualcoachtest.service \
  --property MainPID,ActiveState,SubState,FragmentPath \
  >"$EV/production-seal/service-before.txt"
chmod 600 "$EV/production-seal/service-before.txt"
```

The seal helper must use `lstat`, reject symlinks/hardlinks where authorities require private regular files, hash bytes without serializing secrets, and emit only path labels, modes, sizes, and SHA-256 values.

---

## 3. RED/GREEN command discipline

Use these helpers for every behavioral increment:

```bash
run_red() {
  out=$1
  shift
  set +e
  "$@" 2>&1 | tee "$out"
  rc=${PIPESTATUS[0]}
  set -e
  printf '\nEXIT_CODE=%s\n' "$rc" >>"$out"
  test "$rc" -eq 1       # assertion failure only; not collection/config error
}

run_green() {
  out=$1
  shift
  set +e
  "$@" 2>&1 | tee "$out"
  rc=${PIPESTATUS[0]}
  set -e
  printf '\nEXIT_CODE=%s\n' "$rc" >>"$out"
  test "$rc" -eq 0
}
```

All three mandatory modules must be created and observed failing before production edits:

```bash
run_red "$EV/red-consent-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_nutrition_onboarding_recovery.py

run_red "$EV/red-production-preflight.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_production_preflight.py

run_red "$EV/red-publication-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_publication_recovery.py

git diff --name-only "$BASE" -- \
  gateway hermes_cli dualcoach/profile scripts pyproject.toml \
  | tee "$EV/red/production-files-before-first-edit.txt"
test ! -s "$EV/red/production-files-before-first-edit.txt"
```

Tests must target existing public behavior so RED is an assertion failure, not an import failure for a not-yet-created interface.

---

## 4. Disjoint implementation lanes

These lanes may run concurrently only while their ownership remains disjoint.

| Lane | Exclusive files | Depends on |
|---|---|---|
| A: consent saga/package | `test_telegram_nutrition_onboarding_recovery.py`, `telegram_customer_bootstrap.py`, `telegram_customer_bootstrap_registration.py`, new `dualcoach_profile_package.py`, necessary profile onboarding store files | Mandatory RED |
| B: production preflight | `test_telegram_production_preflight.py`, `hermes_cli/env_loader.py`, `gateway/run.py`, `gateway/config.py`, `nutrition_coaching_config.py`, `telegram_staff_membership_gate.py`, new preflight/menu modules, `dualcoach/profile/pyproject.toml` | Mandatory RED |
| C: receipt/publication | `test_telegram_publication_recovery.py`, `telegram_polling_receipts.py`, publication outbox, runtime publication and transport modules | Mandatory RED |
| D: adapter integration | `telegram.py`, onboarding runtime/callback/collection wiring | A, B, C |
| E: fake API/E2E | `tests/support/fake_telegram_bot_api.py`, fake API tests, E2E and child-process scripts | D interfaces frozen |
| F: migration/artifacts | migration planner, copied-profile migrator, candidate verifier and migration tests | Persisted schemas frozen |

No lane except D edits `telegram.py`. No file is passed between simultaneous owners.

---

## 5. Ordered file-level increments

### Increment 1: authoritative profile package loading

Tests:

- Correct candidate loads whether or not unrelated paths precede it.
- A pre-imported `checkin_cli` outside the expected root fails closed.
- A module whose `__file__` escapes the root fails closed.
- Task26 candidate/source identity mismatch fails closed.
- No `sys.modules` eviction occurs.

Files:

- Add `gateway/platforms/dualcoach_profile_package.py`.
- Replace local `sys.path.insert` logic in bootstrap registration and onboarding runtime.
- Lane D later routes remaining `telegram.py` profile imports through it.

Contract:

1. `DUALCOACH_PROFILE_PACKAGE` names an absolute distribution root containing `checkin_cli/`.
2. Production nutrition mode requires that explicit root; no fallback to `profile/workspace`.
3. Resolve it without following symlinks.
4. Validate it through `load_task26_production_authority`.
5. If `checkin_cli` is already imported, every loaded `checkin_cli.*` module must be under that exact root.
6. Mismatch aborts before registry, workflow, or network mutation.

Do not silently replace an already imported wrong package.

### Increment 2: durable consent handoff saga

Add a backward-readable bootstrap schema with a nullable `ConsentHandoff`:

- session and bootstrap generation;
- update ID and callback query ID;
- callback data or authenticated callback digest;
- actor user ID;
- chat/topic/message IDs;
- consent event timestamp;
- customer key and registry authority digest;
- consent-card message ID;
- canonical handoff digest;
- completed onboarding session digest when available.

State sequence:

1. Validate callback route, actor, session, generation and signature.
2. CAS-persist the handoff while still `AWAITING_CONSENT`.
3. Commit registry consent idempotently.
4. Call onboarding `start_or_resume` using only persisted handoff evidence.
5. Verify the resulting session belongs to the same customer and authority digest.
6. CAS transition to `AWAITING_ACTIVATION`, recording the onboarding session digest.
7. Publish/reconcile the first question.
8. Mark the ingress receipt complete or recovered.

Startup recovery scans:

- `AWAITING_CONSENT` with a valid handoff;
- `AWAITING_ACTIVATION` with an incomplete publication;
- collecting sessions with pending outbox records.

It must not infer consent from registry state alone. A legacy missing-session state without authenticated event evidence fails closed and is reported by migration preflight.

Required nodes include:

```text
test_committed_consent_recovers_missing_session_after_restart_once
test_concurrent_consent_replay_does_not_duplicate_first_question
test_consent_recovery_preserves_original_event_provenance
test_consent_recovery_rejects_mismatched_actor_route_or_generation
test_consent_business_commit_heals_failed_ingress_receipt
test_wrong_preimported_profile_package_fails_before_consent_mutation
```

### Increment 3: failed-ingress reconciliation

Change `telegram_polling_receipts.py` so:

- Valid failure reasons, including `handler_exception`, are durably recorded.
- An exact registered business-recovery candidate may transition:
  `FAILED(handler_exception) -> RECOVERED`.
- No generic retry or arbitrary failed-to-success transition is allowed.
- Reconciliation matches update ID, actor, chat/topic/message, callback digest and business receipt digest.
- A recovered update is terminal and advances the offset exactly once.
- New update IDs replaying the same callback are stopped by business CAS/idempotency, not timing.

A receipt write failure after a successful business commit must attempt the emergency receipt path. If all receipt storage is unavailable, the business authorities still prevent duplicate effects on restart.

### Increment 4: publication recovery

Use an outbox uniqueness key over:

```text
session_id
publication_generation
audience
route_digest
payload_digest
```

Outbox states:

```text
PREPARED -> DISPATCHING -> SENT
                         -> UNKNOWN
```

Rules:

- `PREPARED` is durable before network I/O.
- `DISPATCHING` is durable before `sendMessage` or `editMessageText`.
- For the first customer question, preserve the consent-card message ID and use `editMessageText`. After a crash, retry the same edit; `message is not modified` confirms the known message.
- For unanchored sends, a restart from `DISPATCHING` becomes `UNKNOWN`; it is not resent automatically.
- A business crash before publication leaves `PREPARED` or no attempt and is safely published once during startup recovery.
- Concurrent callback replay must converge on one outbox key and one terminal business transition.

Required nodes include:

```text
test_first_question_edit_recovers_after_response_before_receipt
test_unanchored_send_becomes_unknown_without_duplicate_retry
test_customer_completion_restart_emits_one_owner_card
test_owner_approval_restart_emits_one_operator_delivery
test_concurrent_replay_converges_on_one_publication_receipt
test_recovered_business_commit_advances_offset_once
```

### Increment 5: deterministic production preflight

#### Token authority

At gateway process entry, snapshot a non-empty externally supplied `TELEGRAM_BOT_TOKEN` before dotenv loading. Extend `load_hermes_dotenv` with an explicit protected mapping and restore protected values after:

- user dotenv;
- project dotenv;
- external secret sources;
- managed environment;
- runtime reload.

Do not change precedence globally for unrelated credentials.

#### Bot identity

Require:

```yaml
platforms:
  telegram:
    extra:
      production_preflight:
        expected_bot_id: <integer>
        expected_bot_username: <username without @>
```

`getMe` must exactly match both fields after case-normalizing the username. Mismatch aborts `connect()`.

#### Route readiness

Build canonical owner/customer/operator route triples. Require:

- routes are pairwise distinct;
- `getChat` returns the expected private/forum chat kind;
- bot is `administrator` or `creator` in every group;
- configured topic IDs are valid positive IDs where required;
- `sendChatAction` succeeds for each configured topic as the least-persistent Bot API topic probe;
- customer absence and staff-membership checks remain enforced.

No customer message is sent during preflight.

#### Command surface

When production nutrition is enabled:

- Delete inherited default/all-private/all-group generic command scopes.
- Customer chat scope contains only `/start`.
- Staff chat scope contains only implemented operations commands:
  `/nutritionops` and `/nutritionpreview`.
- Disable lazy generic forum command registration for this candidate.
- Any command registration failure aborts startup instead of logging a warning.

#### API endpoint safety

A configurable Bot API base URL is accepted only when:

- disposable E2E mode is explicitly enabled; and
- hostname resolves exclusively to loopback.

Production mode requires Telegram’s expected HTTPS endpoint. Redirects to non-loopback hosts are rejected by the fake transport harness.

Required nodes include:

```text
test_service_environment_token_survives_startup_and_runtime_reload
test_getme_id_mismatch_refuses_connect
test_getme_username_mismatch_refuses_connect
test_non_admin_bot_refuses_connect
test_invalid_duplicate_or_unreachable_topic_refuses_connect
test_candidate_profile_identity_mismatch_refuses_connect
test_customer_command_scope_is_exact_and_generic_commands_are_absent
test_non_loopback_fake_api_override_refuses_connect
```

### Increment 6: finish mandatory GREEN gates

Run in dependency order:

```bash
run_green "$EV/green-consent-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_nutrition_onboarding_recovery.py

run_green "$EV/green-production-preflight.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_production_preflight.py

run_green "$EV/green-publication-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_publication_recovery.py
```

---

## 6. Disposable fake Telegram Bot API

Implement a stateful loopback HTTP server, not a collection of mocked bot objects.

Required Bot API methods:

- `getMe`
- `getChat`
- `getChatMember`
- `getUpdates`
- `sendMessage`
- `editMessageText`
- `answerCallbackQuery`
- `sendChatAction`
- `setMyCommands`
- `deleteMyCommands`
- `getWebhookInfo`
- `deleteWebhook`

State model:

- configured bot ID/username;
- chats, forum topics and membership statuses;
- monotonic message IDs per chat;
- queued updates with callback query IDs;
- requested polling offsets;
- stored messages and edit history;
- callback answers;
- command scopes;
- append-only sanitized call log;
- exact event signals for fault orchestration.

Telegram-compatible behavior must include:

- `getUpdates(offset=N)` returns only updates with `update_id >= N`;
- identical `editMessageText` returns HTTP 400 with `message is not modified`;
- invalid topics and non-admin routes fail;
- callback IDs and offsets are retained in evidence;
- no token or Authorization value is logged.

No test uses sleeps. The server exposes exact in-process conditions/futures so the parent waits for a named call or state transition with a bounded timeout.

### E2E process model

`scripts/run_dualcoach_v111_disposable_e2e.py` must:

1. Build or locate the pinned candidate Hermes and profile wheels.
2. Create a disposable venv and install with `--no-index --no-deps`.
3. Create a synthetic private profile and registries.
4. Start the fake API on loopback.
5. Launch a child gateway using the real polling/adapter surface.
6. Drive invite claim, consent, all onboarding questions, owner approval, activation, and one daily check-in.
7. Use an E2E-only child wrapper to terminate with `os._exit(86)` at exact post-commit boundaries. Production code contains no fault hooks.
8. Restart from the same durable state.
9. Enqueue concurrent/replayed callbacks before releasing a barrier.
10. Assert exact visible-message counts, outbox receipts, ingress receipts, workflow state, activation state, command scopes and offsets.
11. Verify all socket destinations were loopback.
12. Tear down child processes, server, temp profile and venv.
13. Print `DISPOSABLE_E2E_PASS` as the final line.

Required execution:

```bash
cd "$WT"
python scripts/run_dualcoach_v111_disposable_e2e.py \
  --evidence-dir .omo/evidence/v1.1.1-stability/disposable-e2e \
  2>&1 | tee "$EV/disposable-e2e.txt"

test "${PIPESTATUS[0]}" -eq 0
tail -n 1 "$EV/disposable-e2e.txt" | grep -qx 'DISPOSABLE_E2E_PASS'
```

Evidence directory:

```text
candidate-binding.json
fake-api-calls.jsonl
gateway-process-events.jsonl
network-boundary.json
polling-offsets.json
publication-counts.json
receipt-assertions.json
state-before-crash.json
state-after-restart.json
cleanup.json
```

---

## 7. Migration dry-run and rollback

Add:

```text
scripts/plan_dualcoach_v111_migration.py
scripts/migrate_dualcoach_v111_profile.py
tests/gateway/test_dualcoach_v111_migration.py
docs/dualcoach-v1.1.1-migration.md
```

### Read-only production dry-run

```bash
python scripts/plan_dualcoach_v111_migration.py \
  --profile-root /home/cube/.hermes/profiles/dualcoachtest \
  --service-unit hermes-gateway-dualcoachtest.service \
  --expected-main-pid 3771761 \
  --candidate-binding "$EV/build/candidate-binding.json" \
  --dry-run \
  --output "$EV/migration/dry-run.json"

chmod 600 "$EV/migration/dry-run.json"
test "$(systemctl --user show \
  hermes-gateway-dualcoachtest.service \
  --property MainPID --value)" = 3771761
```

The dry-run must validate and hash, but never rewrite:

- current registry and consent;
- bootstrap state/generation;
- workflow answers and cursor;
- current question/publication generation;
- all publication and ingress receipts;
- activation authority;
- candidate/profile package binding;
- systemd executable and environment authority;
- expected bot identity and routes;
- rollback artifact availability.

It must fail if:

- the live state has a missing onboarding session without authenticated handoff evidence;
- any copied-profile migration would lose answers, cursor, receipts or activation state;
- the old v1.1.0 artifact is unavailable;
- `MainPID` differs;
- output would include a token or raw secret.

### Deployment method emitted by the dry-run

The JSON and document must emit fully resolved commands for a later owner-authorized deployment:

1. Recheck seal and PID.
2. Confirm customer and delivery remain disabled.
3. Stop the service and prove `MainPID=0`.
4. Snapshot and fsync the current profile.
5. Create a new candidate venv; do not mutate the old venv.
6. Install both candidate wheels with `--no-index --no-deps` and verify SHA-256.
7. Copy the profile into a candidate-specific staging root.
8. Run the offline migrator against the copy.
9. Compare semantic digests for registry, consent, answers, cursor, receipts and activation.
10. Point a staged systemd drop-in to the new venv and copied profile.
11. Start the candidate with customer delivery still disabled.
12. Run identity/admin/topic/package/menu preflight.
13. Run operator-only smoke checks.
14. Require separate owner approval before customer enablement.

The old deployed profile and venv remain byte-identical.

### Rollback

If any candidate gate fails:

1. Stop the candidate.
2. Restore the previous systemd drop-in atomically.
3. Point back to the old venv and untouched old profile.
4. Reload systemd and start v1.1.0.
5. Verify old wheel/profile hashes and authority.
6. Confirm customer and delivery remain disabled.
7. Retain the failed staged copy as private evidence; never merge it into the old profile.

This avoids asking v1.1.0 to parse the new bootstrap schema.

---

## 8. Handling the prior 59 failures

Run one serial baseline suite from a second detached worktree at `BASE`, then one final candidate suite. Do not use retries as proof.

For every failure, generate a row containing:

```text
nodeid
first assertion/error
changed-domain relationship
baseline result
classification
owner
required fix
RED evidence path
GREEN evidence path
```

Classifications:

1. Product defect exposed by v1.1.1: add focused RED, fix production.
2. Nondeterministic test: replace timing with an exact event/barrier and bounded timeout.
3. Defective stale assertion: update only after mapping it to an authoritative contract; retain or strengthen behavioral coverage.
4. Environment/setup defect: repair the pinned setup, not the test.
5. Unrelated pre-existing defect: still fix before release or block release; do not waive it.

Forbidden resolutions:

- skip/xfail;
- repeated reruns until pass;
- fixed sleeps or polling delays;
- deleting assertions;
- broad mocks that bypass the integration;
- type/lint suppressions;
- increasing timeouts to hide a race.

Audit new test code:

```bash
git diff --unified=0 "$BASE" -- tests scripts \
  | grep '^+' \
  | grep -E 'time\.sleep|asyncio\.sleep|pytest\.skip|pytest\.xfail|flaky' \
  && exit 1 || true
```

---

## 9. Regression and static gates

Broad required regressions:

```bash
run_green "$EV/regression/required-broad.txt" \
  timeout --signal=TERM --kill-after=30s 3600s \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_nutrition_coaching.py \
  tests/gateway/test_telegram_customer_bootstrap_invite_ttl.py \
  tests/gateway/test_telegram_physique_checkin.py
```

Profile suite:

```bash
run_green "$EV/regression/profile.txt" \
  timeout --signal=TERM --kill-after=30s 3600s \
  .venv/bin/python -m pytest -q dualcoach/profile/tests
```

Full Gateway suite, once:

```bash
run_green "$EV/regression/gateway-full.txt" \
  timeout --signal=TERM --kill-after=30s 7200s \
  .venv/bin/python -m pytest -q tests/gateway \
  --junitxml="$EV/regression/gateway-full.xml"
```

### Basedpyright strict

Generate a config whose `include` is exactly every changed production Python file, including modified legacy seams. Do not use `exclude`, `ignore`, `type: ignore`, `pyright: ignore`, or weakened diagnostic rules.

```json
{
  "typeCheckingMode": "strict",
  "pythonVersion": "3.12",
  "pythonPlatform": "Linux",
  "extraPaths": [".", "dualcoach/profile"],
  "include": ["<every changed production .py path>"]
}
```

```bash
test "$(basedpyright --version | head -n1)" = "basedpyright 1.39.9"

basedpyright \
  --project "$EV/typecheck/basedpyrightconfig.json" \
  2>&1 | tee "$EV/typecheck/basedpyright.txt"
test "${PIPESTATUS[0]}" -eq 0

.venv/bin/ruff check $(git diff --name-only "$BASE" -- '*.py') \
  2>&1 | tee "$EV/typecheck/ruff.txt"
test "${PIPESTATUS[0]}" -eq 0

.venv/bin/python -m compileall -q \
  gateway hermes_cli dualcoach/profile/checkin_cli scripts
```

If strict diagnostics in a touched monolith cannot be resolved without broad unrelated edits, move the new behavior behind a small typed seam, leaving only minimal delegation in the monolith. Do not suppress diagnostics.

### Dependency pinning

Update `dualcoach/profile/pyproject.toml` to:

```toml
requires = ["setuptools==81.0.0"]
dependencies = [
  "jsonschema==4.26.0",
  "pydantic==2.13.4",
  "typer==0.24.1"
]
```

Regenerate `uv.lock` only if the root graph changes, and require `uv lock --check`.

---

## 10. Reproducible wheels and independent artifact

Build from two isolated copies of the candidate source manifest with:

```bash
export SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$BASE")"
export PYTHONHASHSEED=0
export TZ=UTC
export LC_ALL=C

uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/a/hermes" "$EV/build/source-a"

uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/a/profile" "$EV/build/source-a/dualcoach/profile"

uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/b/hermes" "$EV/build/source-b"

uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/b/profile" "$EV/build/source-b/dualcoach/profile"

sha256sum "$EV"/build/{a,b}/{hermes,profile}/*.whl \
  | tee "$EV/build/wheel-hashes.txt"
```

Corresponding A/B wheel hashes and ordered member inventories must match.

The v1.1.1 candidate binding must include:

- release label `NutriCoach-v1.1.1`;
- base commit;
- candidate source-manifest hash;
- candidate patch hash;
- `uv.lock` hash;
- Hermes and profile wheel hashes;
- ordered wheel-member inventory hashes;
- Python, uv, setuptools and basedpyright identities;
- mandatory RED and GREEN evidence hashes;
- broad/full JUnit hashes;
- disposable E2E evidence-root hash;
- migration dry-run hash;
- production before/after seal hashes;
- prior v1.1.0 rollback artifact hash.

Create new v1.1.1 evidence only. Never edit:

```text
dualcoach/releases/v1.1.0/source-golden-bundle
dualcoach/releases/v1.1.0/installed-golden-bundle
dualcoach/releases/v1.1.0/qualification-*
dualcoach/releases/v1.1.0/release-verdict.md
```

Gate:

```bash
git diff --exit-code "$BASE" -- dualcoach/releases/v1.1.0
```

The independent verifier must run from an empty temporary directory, install only the bound local wheels, recompute every hash, execute the installed fake-API E2E, and finish with a unique pass marker.

---

## 11. Acceptance matrix

| Criterion | Primary evidence |
|---|---|
| Consent survives crash before onboarding creation | `green-consent-recovery.txt` |
| Missing session rebuilt only from authenticated provenance | consent recovery tests and migrated-state report |
| Replay/concurrency does not duplicate first question | consent and publication recovery tests |
| Failed ingress becomes exact recovered terminal receipt | publication recovery receipt assertions |
| Token survives dotenv/runtime reload | `green-production-preflight.txt` |
| Bot ID/username/admin/topic/package checks fail closed | production preflight tests |
| Customer-specific command surface | command-scope assertions and fake API log |
| One owner card and one operator delivery after business crash | publication recovery and disposable E2E |
| Ambiguous unanchored send is not duplicated | `UNKNOWN` publication test |
| Full real adapter/polling path | disposable fake API E2E |
| Existing answers/cursor/receipts preserved | migration semantic-digest report |
| Strict typing, lint and compilation | `typecheck/` |
| Prior failures explicitly resolved | failure classification manifest plus full JUnit |
| Reproducible independently runnable wheels | `build/` and candidate binding |
| Production untouched during implementation | identical production seals and PID |
| Rollback remains byte-identical | migration dry-run rollback section |

---

## 12. Final immutability and cleanup gates

Before reporting implementation complete:

```bash
test "$(systemctl --user show \
  hermes-gateway-dualcoachtest.service \
  --property MainPID --value)" = 3771761

python scripts/plan_dualcoach_v111_migration.py \
  --profile-root /home/cube/.hermes/profiles/dualcoachtest \
  --service-unit hermes-gateway-dualcoachtest.service \
  --expected-main-pid 3771761 \
  --candidate-binding "$EV/build/candidate-binding.json" \
  --dry-run \
  --output "$EV/production-seal/final.json"

python scripts/compare_dualcoach_v111_seals.py \
  "$EV/production-seal/initial.json" \
  "$EV/production-seal/final.json" \
  | tee "$EV/production-seal/compare.txt"

grep -qx 'PRODUCTION_IMMUTABILITY_PASS' \
  "$EV/production-seal/compare.txt"

git diff --exit-code "$BASE" -- dualcoach/releases/v1.1.0
test -z "$(git status --porcelain | grep -E '(^|/)(__pycache__|\.pytest_cache|build|dist)/')"

! pgrep -af 'fake_telegram_bot_api|_dualcoach_v111_gateway_process' \
  >"$EV/cleanup-processes.txt"

git worktree list --porcelain \
  | grep -v "$WT" \
  | grep 'nutricoach-v111-baseline' \
  && exit 1 || true
```

Keep the uncommitted candidate worktree and private evidence for review. Remove only baseline worktrees, temporary source copies, fake profiles, child venvs, sockets and processes. Do not commit, push, tag, publish, deploy, restart the production gateway, or call real Telegram.

## Planning evidence

- Planning worktree was detached at `ccd2eee8a97a13d25c36c1446ed5b5aca2e88653`.
- It remained clean.
- It was removed and pruned successfully: `PLANNING_WORKTREE_CLEANUP_PASS`.
- Source checkout remains at the same HEAD and clean.
- Available strict checker: `basedpyright 1.39.9`.
- No product file, production file, service, secret, Telegram endpoint, commit, tag, push, or release was changed or invoked.


I read this as a request for the final advisory implementation wave plan - I will give the ordered RED-to-GREEN sequence, ownership topology, commands, E2E design, and deployment-safety gates without editing files.

# Decision

Implement a recovery saga using the existing bootstrap ledger, customer registry, onboarding store, publication outbox, and polling receipt gate.

Reject a new unified transaction journal: it would require broader schema migration, duplicate existing authorities, and materially increase rollback risk.

For profile imports, use a single fail-closed resolver. Never evict or replace existing `checkin_cli` modules in `sys.modules`; reject startup if already-loaded modules do not come from the sealed candidate root.

## Delivery boundary

Telegram has no idempotency key for `sendMessage`. Therefore:

- Known-message `editMessageText` can be retried safely.
- An ambiguous unanchored `sendMessage` must become durable `UNKNOWN` and must not be retried automatically.
- Business-commit-before-send crashes can recover and publish exactly once.
- Tests must not claim a recoverable provider receipt when Telegram accepted an unanchored send but the process lost the response.

---

# 1. Worktree and evidence setup

```bash
set -euo pipefail
umask 077

BASE=ccd2eee8a97a13d25c36c1446ed5b5aca2e88653
SRC=/home/cube/projects/richard/hermes-agent-dualcoach-v1.1
WT=/home/cube/projects/richard/.worktrees/nutricoach-v111-impl
EV="$WT/.omo/evidence/v1.1.1-stability"

test -z "$(git -C "$SRC" status --porcelain)"
test ! -e "$WT"
git -C "$SRC" worktree add --detach "$WT" "$BASE"

cd "$WT"
mkdir -p "$EV"/{red,green,baseline,regression,typecheck,build,migration,production-seal}
uv venv --python 3.12 .venv
uv sync --frozen --extra dev --extra messaging
uv pip install --python .venv/bin/python --no-deps -e dualcoach/profile

test "$(git rev-parse HEAD)" = "$BASE"
test -z "$(git status --porcelain)"
```

No commit, push, tag, release, production restart, real Telegram request, or deployed-file edit is permitted.

Use:

```bash
run_red() {
  out=$1; shift
  set +e
  "$@" 2>&1 | tee "$out"
  rc=${PIPESTATUS[0]}
  set -e
  printf '\nEXIT_CODE=%s\n' "$rc" >>"$out"
  test "$rc" -eq 1
}

run_green() {
  out=$1; shift
  set +e
  "$@" 2>&1 | tee "$out"
  rc=${PIPESTATUS[0]}
  set -e
  printf '\nEXIT_CODE=%s\n' "$rc" >>"$out"
  test "$rc" -eq 0
}
```

RED exit codes `2`, `3`, `4`, or `5` are invalid because they indicate collection, setup, or selection failure rather than behavioral RED.

---

# 2. Dependency order

```text
W0 isolation, baseline seal, mandatory RED
 |
 +--> W1-A package resolver and consent ledger
 |
 +--> W1-B token/preflight/menu
 |
 +--> W1-C ingress receipts and publication outbox
 |
 `--> W1-E fake API foundation

W1-A + W1-B + W1-C
 |
 v
W2 adapter/runtime integration, exclusively owned
 |
 v
W3 mandatory GREEN
 |
 +--> W4-A disposable installed-wheel E2E
 |
 `--> W4-B migration planner/copied-profile migrator

W4-A + W4-B
 |
 v
W5 prior-failure resolution and regressions
 |
 v
W6 strict typing, reproducible builds, independent verifier
 |
 v
W7 production immutability comparison and cleanup
```

---

# 3. Disjoint delegation topology

| Lane | Exclusive write ownership |
|---|---|
| A: consent/package | `tests/gateway/test_telegram_nutrition_onboarding_recovery.py`, `gateway/platforms/dualcoach_profile_package.py`, `telegram_customer_bootstrap.py`, `telegram_customer_bootstrap_registration.py`, necessary `dualcoach/profile/checkin_cli/nutrition_onboarding*.py` |
| B: preflight | `tests/gateway/test_telegram_production_preflight.py`, `hermes_cli/env_loader.py`, `gateway/run.py`, `gateway/config.py`, `nutrition_coaching_config.py`, `telegram_staff_membership_gate.py`, new preflight/menu modules, `dualcoach/profile/pyproject.toml` |
| C: publication | `tests/gateway/test_telegram_publication_recovery.py`, `telegram_polling_receipts.py`, publication outbox, runtime publication and transport modules |
| D: integration | `gateway/platforms/telegram.py`, `telegram_nutrition_onboarding_runtime.py`, runtime callback/collection wiring |
| E: E2E | `tests/support/fake_telegram_bot_api.py`, fake API tests, E2E driver and child process wrapper |
| F: migration/artifacts | migration planner, copied-profile migrator, migration tests, candidate verifier |

Rules:

- Only lane D edits `telegram.py`.
- Lane D starts after A, B, and C freeze their interfaces.
- No simultaneous lane may edit another lane’s file.
- Cross-lane change requests are queued for the owning lane.

---

# 4. Wave W0: mandatory behavioral RED

Create the three mandatory test modules before any production edit.

```bash
run_red "$EV/red-consent-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_nutrition_onboarding_recovery.py

run_red "$EV/red-production-preflight.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_production_preflight.py

run_red "$EV/red-publication-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_publication_recovery.py

git diff --name-only "$BASE" -- \
  gateway hermes_cli dualcoach/profile scripts pyproject.toml \
  >"$EV/red/production-files-before-first-edit.txt"

test ! -s "$EV/red/production-files-before-first-edit.txt"
```

Required RED cases:

### Consent recovery

```text
test_committed_consent_recovers_missing_session_after_restart_once
test_concurrent_consent_replay_does_not_duplicate_first_question
test_consent_recovery_preserves_original_event_provenance
test_consent_recovery_rejects_mismatched_actor_route_or_generation
test_consent_business_commit_heals_failed_ingress_receipt
test_wrong_preimported_profile_package_fails_before_mutation
```

### Production preflight

```text
test_service_token_survives_startup_and_runtime_dotenv_reload
test_getme_id_mismatch_refuses_connect
test_getme_username_mismatch_refuses_connect
test_non_admin_bot_refuses_connect
test_invalid_duplicate_or_unreachable_topic_refuses_connect
test_candidate_package_identity_mismatch_refuses_connect
test_customer_command_scope_is_exact
test_non_loopback_test_api_override_refuses_connect
```

### Publication recovery

```text
test_first_question_edit_recovers_after_response_before_receipt
test_unanchored_send_becomes_unknown_without_duplicate_retry
test_customer_completion_restart_emits_one_owner_card
test_owner_approval_restart_emits_one_operator_delivery
test_concurrent_replay_converges_on_one_publication_receipt
test_recovered_business_commit_advances_offset_once
```

Tests must use current public behavior. A missing future import is not acceptable RED.

---

# 5. Wave W1-A: profile package and consent saga

## Increment A1: sealed package resolver

Add:

```text
gateway/platforms/dualcoach_profile_package.py
```

Modify:

```text
gateway/platforms/telegram_customer_bootstrap_registration.py
gateway/platforms/telegram_nutrition_onboarding_runtime.py
```

Contract:

1. `DUALCOACH_PROFILE_PACKAGE` is required in production nutrition mode.
2. It names an absolute distribution root containing `checkin_cli/`.
3. Reject symlinks and path escape.
4. Validate the root through Task26 candidate authority.
5. If any `checkin_cli` module is already imported, every such module must resolve under the candidate root.
6. Wrong-source imports abort before registry, workflow, or network mutation.
7. Never delete or replace entries in `sys.modules`.
8. Remove fallback selection from `profile/workspace`.

Run focused RED/GREEN around package tests before proceeding.

## Increment A2: durable consent handoff

Modify:

```text
gateway/platforms/telegram_customer_bootstrap.py
gateway/platforms/telegram_customer_bootstrap_registration.py
dualcoach/profile/checkin_cli/nutrition_onboarding.py
dualcoach/profile/checkin_cli/nutrition_onboarding_store.py  # only if required
```

Add a backward-readable bootstrap schema containing nullable `ConsentHandoff`:

```text
session_id
bootstrap_generation
customer_key
update_id
callback_query_id
callback_data_digest
actor_user_id
chat_id
topic_id
message_id
consent_card_message_id
event_time_utc
registry_authority_digest
handoff_digest
onboarding_session_digest
```

Transaction sequence:

1. Authenticate callback route, actor, signature and generation.
2. CAS-persist `ConsentHandoff` while state remains `AWAITING_CONSENT`.
3. Commit registry consent idempotently.
4. Start/resume onboarding from persisted evidence.
5. Verify customer and authority bindings.
6. CAS record the onboarding session digest.
7. Transition to `AWAITING_ACTIVATION`.
8. Reconcile first-question publication.
9. Reconcile the ingress receipt.

Startup recovery scans:

- `AWAITING_CONSENT` with a valid handoff;
- `AWAITING_ACTIVATION` with incomplete publication;
- collecting sessions with pending outbox work.

A legacy missing-session state without authenticated handoff evidence must fail closed. It must not infer provenance from registry consent alone.

---

# 6. Wave W1-B: token, identity, topics, package, menu

## Increment B1: token authority

Modify:

```text
hermes_cli/env_loader.py
gateway/run.py
tests/gateway/test_telegram_production_preflight.py
```

At process entry, snapshot a non-empty service-provided `TELEGRAM_BOT_TOKEN`. Extend `load_hermes_dotenv` with an explicit protected-value mapping and restore protected values after:

- user dotenv;
- project dotenv;
- external secret sources;
- managed environment;
- runtime reload.

Do not globally alter precedence for unrelated credentials.

## Increment B2: identity and route preflight

Add:

```text
gateway/platforms/telegram_production_preflight.py
gateway/platforms/telegram_nutrition_menu.py
```

Modify:

```text
gateway/config.py
gateway/platforms/nutrition_coaching_config.py
gateway/platforms/telegram_staff_membership_gate.py
```

Required configuration:

```yaml
platforms:
  telegram:
    extra:
      production_preflight:
        expected_bot_id: <positive integer>
        expected_bot_username: <username without @>
```

Before polling or customer delivery:

1. `getMe` must match both expected fields.
2. Build canonical owner/customer/operator route triples.
3. Require pairwise-distinct route triples.
4. `getChat` must confirm expected private/forum chat type.
5. `getChatMember` must show bot `administrator` or `creator` for groups.
6. `sendChatAction` must validate configured topic reachability without sending a message.
7. Existing staff/customer separation gates remain mandatory.
8. Candidate package identity must match Task26 authority.
9. Any failure aborts `connect()`.

## Increment B3: deterministic menu

Production nutrition command scopes:

- Delete inherited default/all-private/all-group generic commands.
- Customer chat scope: `/start` only.
- Staff chat scope: `/nutritionops`, `/nutritionpreview`.
- Disable generic lazy forum command registration for this candidate.
- `setMyCommands`/`deleteMyCommands` failures are fatal.

## Increment B4: pinned profile graph

Change `dualcoach/profile/pyproject.toml` to exact pins:

```toml
[build-system]
requires = ["setuptools==81.0.0"]
build-backend = "setuptools.build_meta"

dependencies = [
  "jsonschema==4.26.0",
  "pydantic==2.13.4",
  "typer==0.24.1"
]
```

---

# 7. Wave W1-C: ingress and publication

## Increment C1: failed ingress reconciliation

Modify:

```text
gateway/platforms/telegram_polling_receipts.py
```

Rules:

- Persist valid failure reasons, including `handler_exception`.
- Permit `FAILED(handler_exception) -> RECOVERED` only for an exact registered business-recovery candidate.
- Match update ID, actor, route, message, callback digest and business receipt digest.
- Recovered receipts are terminal.
- Offset advancement remains monotonic and occurs once.
- Arbitrary failed updates cannot be replayed.
- New update IDs containing a repeated callback are rejected by business CAS/version checks.

## Increment C2: publication state machine

Modify:

```text
gateway/platforms/telegram_nutrition_onboarding_publication_outbox.py
gateway/platforms/telegram_nutrition_onboarding_runtime_publication.py
gateway/platforms/telegram_nutrition_onboarding_runtime_publication_transport.py
```

Outbox key:

```text
session_id
publication_generation
audience
route_digest
payload_digest
```

States:

```text
PREPARED -> DISPATCHING -> SENT
                         -> UNKNOWN
```

Rules:

- Persist `PREPARED` before network I/O.
- Persist `DISPATCHING` before send/edit.
- Preserve consent-card message ID.
- Publish the first customer question by editing that known message.
- Retry identical edit after restart; treat Telegram’s `message is not modified` as confirmation.
- An unanchored send found in `DISPATCHING` after restart becomes `UNKNOWN` and is not resent.
- A business commit that crashed before publication is recoverable to one normal send.
- Concurrent callbacks converge on one outbox key.

---

# 8. Wave W2: adapter integration

Lane D exclusively modifies:

```text
gateway/platforms/telegram.py
gateway/platforms/telegram_nutrition_onboarding_runtime.py
gateway/platforms/telegram_nutrition_onboarding_runtime_callback.py
gateway/platforms/telegram_nutrition_onboarding_runtime_collection.py
```

Integration order:

1. Resolve and authorize profile package before constructing nutrition services.
2. Run token/identity/chat/topic/package preflight before polling.
3. Install deterministic command scopes.
4. Persist consent handoff before registry consent call.
5. On startup, reconcile consent handoffs before ordinary pending publications.
6. Register exact polling business-recovery candidates.
7. Reconcile failed ingress after business recovery.
8. Preserve existing callback acknowledgements and authorization checks.
9. Do not catch the E2E process-crash sentinel in production code; fault injection belongs only to the child harness.

---

# 9. Wave W3: mandatory GREEN

```bash
run_green "$EV/green-consent-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_nutrition_onboarding_recovery.py

run_green "$EV/green-production-preflight.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_production_preflight.py

run_green "$EV/green-publication-recovery.txt" \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_publication_recovery.py
```

Each evidence file must contain all expected node IDs and `EXIT_CODE=0`.

---

# 10. Wave W4-A: disposable fake Telegram Bot API E2E

Add:

```text
tests/support/fake_telegram_bot_api.py
tests/gateway/test_fake_telegram_bot_api.py
scripts/run_dualcoach_v111_disposable_e2e.py
scripts/_dualcoach_v111_gateway_process.py
```

## Fake API behavior

Support:

```text
getMe
getChat
getChatMember
getUpdates
sendMessage
editMessageText
answerCallbackQuery
sendChatAction
setMyCommands
deleteMyCommands
getWebhookInfo
deleteWebhook
```

State:

- bot identity;
- chats, topics and memberships;
- queued updates and callback IDs;
- monotonic message IDs per chat;
- polling offsets;
- sent and edited messages;
- callback acknowledgements;
- command scopes;
- append-only sanitized API call log;
- named synchronization events.

Telegram-compatible details:

- `getUpdates(offset=N)` returns only `update_id >= N`.
- Repeating identical `editMessageText` returns 400 `message is not modified`.
- Invalid topic/admin operations fail.
- Tokens are never written to logs.
- All waits use named events/futures with bounded timeout; no sleep or polling loops.

## Child gateway fault injection

The E2E-only child process imports the real adapter and wraps exact methods before starting the gateway. It calls `os._exit(86)` immediately after configured durable boundaries, including:

- registry consent committed before onboarding session creation;
- business transition committed before ingress receipt;
- known-message edit returned before publication receipt.

No fault hook is added to production modules.

## E2E sequence

1. Build or locate bound candidate wheels.
2. Create disposable venv with no index access.
3. Install Hermes and profile wheels using `--no-deps`.
4. Create synthetic private profile/config/registry.
5. Start loopback fake API.
6. Launch real gateway polling child.
7. Claim invite and commit consent.
8. Crash after registry commit.
9. Restart and verify onboarding session/first question recovery.
10. Inject replay and concurrent callback updates.
11. Complete every onboarding question.
12. Crash after customer completion business commit.
13. Restart and assert one owner card.
14. Approve as owner; crash after approval business commit.
15. Restart and assert one operator delivery.
16. Activate and run one daily check-in.
17. Assert receipts, offsets, command scopes and exact message counts.
18. Assert every socket destination was loopback.
19. Stop all children and delete disposable state.
20. Print `DISPOSABLE_E2E_PASS` as the final line.

Required command:

```bash
cd "$WT"
python scripts/run_dualcoach_v111_disposable_e2e.py \
  --evidence-dir .omo/evidence/v1.1.1-stability/disposable-e2e \
  2>&1 | tee "$EV/disposable-e2e.txt"

test "${PIPESTATUS[0]}" -eq 0
tail -n 1 "$EV/disposable-e2e.txt" | grep -qx 'DISPOSABLE_E2E_PASS'
```

Required artifacts:

```text
disposable-e2e/candidate-binding.json
disposable-e2e/fake-api-calls.jsonl
disposable-e2e/gateway-process-events.jsonl
disposable-e2e/network-boundary.json
disposable-e2e/polling-offsets.json
disposable-e2e/publication-counts.json
disposable-e2e/receipt-assertions.json
disposable-e2e/state-before-crash.json
disposable-e2e/state-after-restart.json
disposable-e2e/cleanup.json
```

---

# 11. Wave W4-B: migration and rollback

Add:

```text
scripts/plan_dualcoach_v111_migration.py
scripts/migrate_dualcoach_v111_profile.py
scripts/compare_dualcoach_v111_seals.py
tests/gateway/test_dualcoach_v111_migration.py
docs/dualcoach-v1.1.1-migration.md
```

## Read-only dry-run

```bash
python scripts/plan_dualcoach_v111_migration.py \
  --profile-root /home/cube/.hermes/profiles/dualcoachtest \
  --service-unit hermes-gateway-dualcoachtest.service \
  --expected-main-pid 3771761 \
  --candidate-binding "$EV/build/candidate-binding.json" \
  --dry-run \
  --output "$EV/migration/dry-run.json"

chmod 600 "$EV/migration/dry-run.json"

test "$(systemctl --user show \
  hermes-gateway-dualcoachtest.service \
  --property MainPID --value)" = 3771761
```

Dry-run validates without rewriting:

- service executable and environment;
- token source without printing token;
- candidate/profile package binding;
- registry and consent;
- bootstrap state/generation/handoff;
- onboarding answers and cursor;
- publication and ingress receipts;
- activation authority;
- old v1.1.0 rollback artifact;
- config, package and state SHA-256 inventory.

It fails if:

- `MainPID != 3771761`;
- a legacy missing workflow lacks authenticated handoff evidence;
- migration would discard answers, cursor, receipts or activation;
- old rollback artifacts are missing;
- any output contains a token/raw secret.

## Copied-profile migration

Later deployment must never migrate the old profile in place:

1. Stop service and verify `MainPID=0`.
2. Snapshot and fsync the old profile.
3. Create a candidate-specific venv.
4. Install wheels with `--no-index --no-deps`.
5. Copy the profile to a candidate staging root.
6. Migrate only the copy.
7. Compare semantic digests of registry, consent, answers, cursor, receipts and activation.
8. Point a staged systemd drop-in to the new venv/profile.
9. Start with customer delivery disabled.
10. Run production preflight and operator-only smoke.
11. Require separate approval before customer enablement.

## Rollback

1. Stop candidate.
2. Restore old systemd drop-in.
3. Point to untouched old venv/profile.
4. Start v1.1.0.
5. Verify old artifact and profile hashes.
6. Confirm customer/delivery remain disabled.
7. Preserve failed candidate copy as private evidence.

---

# 12. Wave W5: prior failures and regressions

For each previously observed failure, emit a manifest row:

```text
nodeid
first failure
baseline result
classification
owner
required fix
RED evidence
GREEN evidence
```

Classifications:

- product defect;
- nondeterministic test defect;
- stale/incorrect assertion;
- environment/setup defect;
- unrelated pre-existing defect.

Every category must be fixed or block release. No skips, xfails, retries-until-pass, sleeps, weakened assertions, broad mocks, or timeout inflation.

Audit:

```bash
git diff --unified=0 "$BASE" -- tests scripts \
  | grep '^+' \
  | grep -E 'time\.sleep|asyncio\.sleep|pytest\.skip|pytest\.xfail|flaky' \
  && exit 1 || true
```

Required regressions:

```bash
run_green "$EV/regression/required-broad.txt" \
  timeout --signal=TERM --kill-after=30s 3600s \
  .venv/bin/python -m pytest -q \
  tests/gateway/test_nutrition_coaching.py \
  tests/gateway/test_telegram_customer_bootstrap_invite_ttl.py \
  tests/gateway/test_telegram_physique_checkin.py

run_green "$EV/regression/profile.txt" \
  timeout --signal=TERM --kill-after=30s 3600s \
  .venv/bin/python -m pytest -q dualcoach/profile/tests

run_green "$EV/regression/gateway-full.txt" \
  timeout --signal=TERM --kill-after=30s 7200s \
  .venv/bin/python -m pytest -q tests/gateway \
  --junitxml="$EV/regression/gateway-full.xml"
```

Run the final full suite once. A failure returns to a focused RED/GREEN increment; do not rerun the full command blindly.

---

# 13. Wave W6: static and artifact qualification

## Strict basedpyright

Generate `"$EV/typecheck/basedpyrightconfig.json"` with every changed production Python file explicitly included:

```json
{
  "typeCheckingMode": "strict",
  "pythonVersion": "3.12",
  "pythonPlatform": "Linux",
  "extraPaths": [".", "dualcoach/profile"],
  "include": ["<all changed production Python paths>"]
}
```

No `exclude`, blanket suppressions, `type: ignore`, or `pyright: ignore`.

```bash
test "$(basedpyright --version | head -n1)" = "basedpyright 1.39.9"

basedpyright --project "$EV/typecheck/basedpyrightconfig.json" \
  2>&1 | tee "$EV/typecheck/basedpyright.txt"
test "${PIPESTATUS[0]}" -eq 0

.venv/bin/ruff check $(git diff --name-only "$BASE" -- '*.py') \
  2>&1 | tee "$EV/typecheck/ruff.txt"
test "${PIPESTATUS[0]}" -eq 0

.venv/bin/python -m compileall -q \
  gateway hermes_cli dualcoach/profile/checkin_cli scripts
```

## Reproducible wheels

Create two isolated copies from the same candidate source manifest.

```bash
export SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$BASE")"
export PYTHONHASHSEED=0
export TZ=UTC
export LC_ALL=C

uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/a/hermes" "$EV/build/source-a"
uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/a/profile" "$EV/build/source-a/dualcoach/profile"

uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/b/hermes" "$EV/build/source-b"
uv build --wheel --no-build-isolation \
  --out-dir "$EV/build/b/profile" "$EV/build/source-b/dualcoach/profile"

sha256sum "$EV"/build/{a,b}/{hermes,profile}/*.whl \
  | tee "$EV/build/wheel-hashes.txt"
```

A/B hashes and ordered wheel inventories must match.

Candidate binding includes:

- `NutriCoach-v1.1.1`;
- base commit;
- candidate source-manifest and patch hashes;
- `uv.lock` hash;
- both wheel hashes and member inventory hashes;
- toolchain identities;
- mandatory RED/GREEN evidence hashes;
- full JUnit hash;
- E2E evidence-root hash;
- migration dry-run hash;
- production before/after seal hashes;
- v1.1.0 rollback artifact hash.

Never alter sealed v1.1.0 artifacts:

```bash
git diff --exit-code "$BASE" -- dualcoach/releases/v1.1.0
```

---

# 14. Wave W7: production immutability and cleanup

Recompute the production seal and compare it byte-for-byte/semantically with the initial seal.

```bash
test "$(systemctl --user show \
  hermes-gateway-dualcoachtest.service \
  --property MainPID --value)" = 3771761

python scripts/plan_dualcoach_v111_migration.py \
  --profile-root /home/cube/.hermes/profiles/dualcoachtest \
  --service-unit hermes-gateway-dualcoachtest.service \
  --expected-main-pid 3771761 \
  --candidate-binding "$EV/build/candidate-binding.json" \
  --dry-run \
  --output "$EV/production-seal/final.json"

python scripts/compare_dualcoach_v111_seals.py \
  "$EV/production-seal/initial.json" \
  "$EV/production-seal/final.json" \
  | tee "$EV/production-seal/compare.txt"

grep -qx 'PRODUCTION_IMMUTABILITY_PASS' \
  "$EV/production-seal/compare.txt"

git diff --exit-code "$BASE" -- dualcoach/releases/v1.1.0

! pgrep -af 'fake_telegram_bot_api|_dualcoach_v111_gateway_process' \
  >"$EV/cleanup-processes.txt"
```

Cleanup requirements:

- remove baseline worktrees;
- remove temporary source copies after their manifest is recorded;
- remove disposable profiles, venvs, sockets and fake servers;
- verify no child gateway remains;
- retain the uncommitted implementation worktree and private evidence for review;
- do not commit, push, tag, publish, deploy, restart production, or contact Telegram.

No files were edited in producing this plan.