# Task 2 evidence — dormant Telegram physique-checkin dispatch seam

## Scope and safety boundary

- Changed only the Hermes Telegram adapter, a small typed private-wizard bridge,
  and hermetic gateway tests.
- Did **not** change `/home/cube/.hermes/config.yaml`, any token, cron job,
  legacy bridge, gateway process, or network state.
- The bridge is absent unless the nested profile-only configuration is exactly
  enabled with `owner_id`, `chat_id`, and `topic_id`. It lazy-loads the
  profile-local check-in package only after that gate passes.

## TDD / characterization

1. Baseline characterization: repository venv command
   `venv/bin/python -m pytest -q tests/gateway/test_telegram_clarify_buttons.py`
   passed (`12 passed`). This pins the pre-existing clarify callback behavior.
2. Red: the new adapter contract test initially failed at collection with
   `ModuleNotFoundError: gateway.platforms.physique_checkin` before the bridge
   module existed.
3. Green/regression:

   ```text
   venv/bin/python -m pytest -q \
     tests/gateway/test_telegram_physique_checkin.py \
     tests/gateway/test_telegram_clarify_buttons.py
   23 passed
   ```

4. `venv/bin/python -m compileall -q gateway/platforms/physique_checkin.py
   gateway/platforms/telegram.py` passed.

The system `pytest` interpreter lacks `pytest-asyncio`; the repository `venv`
contains the declared async test dependency and was used for all gateway
evidence. No package was installed or changed.

## Acceptance coverage

| Requirement | Hermetic evidence |
| --- | --- |
| Feature disabled is a no-op | Missing/false config yields no bridge; disabled `pc1:` callback falls through the existing callback router unchanged. |
| Exact owner/chat/topic boundary | Success requires all three exact values; foreign owner, wrong topic, and wrong prompt-message IDs are rejected. |
| Callback grammar / privacy | `pc1:<32-hex-session>:<step>:<version>:<action>` parser rejects malformed, answer-like, and overlong actions. Every rendered callback is byte-checked at `<=64`; no bodyweight/calorie text is present. |
| Replay/stale/expired safety | Replayed launch, stale version, and expiry inputs are denied before the profile service is mutated. |
| Typed ingress isolation | An active typed value is consumed before `_should_process_message`, text batching, session aggregation, or LLM dispatch. Foreign/wrong-topic typed inputs are denied. |
| Existing topic send semantics | The fake adapter artifact exercises `send_physique_checkin_launcher()` with the normal thread-aware send helper and asserts configured `chat_id` plus `message_thread_id`; no network client is invoked. |
| Existing callbacks unchanged | The 12 pre-existing clarify callback tests remain green. |

## Adapter contract exported for the next wiring task

`TelegramAdapter.send_physique_checkin_launcher(flow="morning")` is dormant
unless the explicit flag is present. It sends one Start card using the existing
topic-aware send helper, then binds the returned Telegram message ID to the
opaque session. Callback prompts edit the same message; typed-input follow-ups
are sent in the same configured topic and re-bound before another button can be
accepted.

## Cleanup / quality

- No live Telegram API call, poller, gateway start/restart, or delivery was
  performed.
- New production bridge is `248` pure non-comment/non-blank LOC (within the
  250-LOC ceiling); the shared Telegram file was only extended at the existing
  callback/text ingress seams.
- Ruff and basedpyright executables are not installed in the repository venv;
  no replacement package was installed. `compileall`, targeted regression, and
  `git diff --check` on task-owned paths passed.

## Verifier follow-up: restart and typed-prompt repair

Two verifier findings were reproduced with red tests before this repair:

1. A fresh bridge had no in-memory prompt binding, so a pending typed answer
   after a gateway restart could have fallen through to generic ingress.
2. The pain/training "입력" actions re-rendered the old choice card instead of
   replacing it with the exact direct question.

The bridge now persists only prompt-address metadata at the owner-only,
atomic profile-local `data/wizard/telegram-bindings.json`: exact owner, chat,
topic, opaque session ID, Telegram message ID, expected step/version, expiry,
and `awaiting_text`. It contains no body/health/training answer values. A fresh
bridge filters this state by the configured exact tuple and expiry before it
will consume an answer; foreign/wrong-topic text remains rejected. Typed-action
cards now edit in place to explicit questions, e.g. pain requests `위치`,
`강도(0~10)`, and `언제부터`; training requests the training content.

Red-to-green follow-up result:

```text
venv/bin/python -m pytest -q \
  tests/gateway/test_telegram_physique_checkin.py \
  tests/gateway/test_telegram_clarify_buttons.py
26 passed
```

The rehydration test uses a real `WizardService` with a pytest temporary state
home and a fresh `PhysiqueCheckinBridge`, starts a bound draft, constructs a
new bridge, then proves the typed numeric value is consumed and advances to the
sleep prompt. It writes no profile data and invokes no network client.

After splitting binding persistence and prompt rendering, production pure LOC
is `physique_checkin.py=247`, `physique_checkin_bindings.py=108`, and
`physique_checkin_prompts.py=41`.
