<analysis>
**Literal Request**: Investigate a Python codebase at /home/cube/projects/richard/hermes-agent read-only to find the exact root cause and minimal test seam for why '보통 수준' (clear Korean activity-level answer) triggered a clarification asking for exact choices, while '보통' passed.
**Actual Need**: Identify the specific code path (canonical enum normalization, field-level parsing, reconciliation/clarification) that rejects the suffix '수준', with exact file/function/line references, plus a minimal regression test that fails now. Prove the root cause from code and callers.
**Success Looks Like**: A concrete diagnosis: which function rejects '보통 수준', where it's called from in production and preview paths, a failing minimal test, and no files edited.
</analysis>

Let me start with parallel exploration of the codebase structure.


The relevant stack is the nutrition onboarding clarification/reconciliation logic. Let me examine the key files.


This is the core clue. Let me examine the domain module, reconciliation logic, and callers in parallel.


Let me locate the actual source of the `checkin_cli` domain package and examine the preview flow and runtime collection callers


The `checkin_cli` domain is an installed/vendored package (only found under evidence snapshots so far). Let me locate its actual source and examine the reconciliation validity logic


Let me examine the production runtime collection/dedup path and locate the checkin_cli package source.


The `checkin_cli` package is an installed/external dependency (gitignored). Let me read the domain reconciliation logic from the evidence snapshot and the reconciler prompt to prove why an un-normalized value triggers re-asking


Let me examine the domain's reconciliation eligibility logic. Let me find the correct evidence path and the reconciliation validation


Now let me read the installed `checkin_cli` domain to confirm how reconciliation eligibility and `submit_answer` treat a raw un-normalized activity value.


Let me examine how the gateway filters provider clarifications against already-valid answers (preview completion and production publication).


The clarification flow is driven by a deterministic `compile_clarification_policy`. Let me read it from the installed snapshot — this is where hypothesis (3) lives


The deterministic policy module `checkin_cli.nutrition_onboarding_clarification_policy` is the crux for hypothesis (3). Let me locate its current source and read the production caller


The deterministic policy `compile_clarification_policy` in the installed `checkin_cli` package is the eligibility gate. Let me locate its current source in the environment and confirm the activity validation logic


Let me read the existing unit test for `parse_answer` and the NATURAL fixture test to pin the exact regression-test seam.


Let me pin the exact line numbers of the activity branch in `parse_answer` and confirm the production caller line numbers


I have the complete causal chain proven from code and callers. I won't modify anything.

<analysis>
**Literal Request**: Find the exact root cause and minimal test seam for why '보통 수준' triggered a clarification asking for exact choices while '보통' passed, evaluating the three hypotheses (canonical-enum normalization, field-level parsing, reconciliation eligibility).
**Actual Need**: A file/function/line diagnosis with a failing regression test, proving which hypothesis is the real cause, plus production and preview caller trace.
**Success Looks Like**: Report with exact references and a minimal RED test; no files edited.
</analysis>

## Root cause (proven)

The failure is at **field-level parsing**, hypothesis (2), which is a concrete instance of hypothesis (1). Hypothesis (3) is refuted.

**`parse_answer("activity_category", text)`** in `gateway/platforms/telegram_nutrition_onboarding_copy.py:165-179` only maps a value to a canonical enum when it is (a) an exact `_ACTIVITY_CATEGORIES` token (`"보통"` → `"moderate"`) or (b) the single hardcoded alias `"<label> 활동량입니다."`. Any other natural phrasing — including `"보통 수준"` — falls through to `parse_legacy_activity_answer` (lines 115-127, which only matches the `"활동 수준: <label>. "` prefix) and is returned **raw, un-normalized**, i.e. the stored value becomes the Korean string `"보통 수준"` instead of `"moderate"` (final line 179: `return legacy[0] if legacy is not None else value`).

Concretely for `"보통 수준"`:
- `_ACTIVITY_CATEGORIES.get("보통 수준")` → `None` (line 166; dict at copy.py:15 has only exact key `"보통"`).
- Alias check `value == f"{label} 활동량입니다."` → `False` for every label (line 172).
- `parse_legacy_activity_answer` returns `None` (line 116-122, requires prefix `활동 수준: ..., `).
- Returns `"보통 수준"` raw (line 179).

## Why that triggers a clarification while '보통' does not

At attestation both production and preview feed the collected answers into a **deterministic** clarification compiler, `compile_clarification_policy`, from the installed `checkin_cli` package:

- Production: `telegram_nutrition_onboarding_runtime_publication.py:75` → `policy = compile_clarification_policy(answers, reference_date=...)`; when the policy state is `"clarifying"` with issues, it builds a `NutritionOnboardingClarification` for the flagged field and re-asks via `render_clarification_text` (lines 94-106). That re-inline prompt is precisely the "clarification asking for exact choices".
- Preview: `telegram_nutrition_onboarding_preview_completion.py:122` → same policy; issues (from `policy.visible_issue_ids`) become clarifications and the phase becomes `"clarifying"` (lines 121-134).

The policy flags an enum field as ambiguous when its stored value is not canonical. With `"보통"` the stored value is `"moderate"` (canonical) → no issue → `"resolved"/"confirming"`. With `"보통 수준"` the stored value is the raw non-canonical string → the policy emits an ambiguity issue for `activity_category` → the exact-choices re-ask.

Evidence that canonical activity values already pass the eligibility gate (refutes hypothesis 3): the repo's own `test_nutrition_onboarding_clarification_policy.py:222-...` (`test_exact_natural_korean_fixture_compiles_without_issues_or_holds`) uses `"보통 활동량입니다."` → parsed to `"moderate"` and asserts `issues == []`, `holds == []`, `state == "resolved"`. So the policy does **not** re-ask valid enum values; it re-asks only because `parse_answer` let an invalid raw value through.

## Caller trace (both paths go through `parse_answer`)

- Production collection: `telegram_nutrition_onboarding_runtime_collection.py:250` `value = parse_answer(status.next_field, raw_value)` → `service.submit_answer(...)`. Clarification-revision path also uses it at line 189.
- Preview: `telegram_nutrition_onboarding_preview_flow.py:92` `parsed = parse_answer(field, str(getattr(message, "text", "")))`.

## Minimal regression test (fails now)

Seam: `tests/gateway/test_telegram_nutrition_onboarding_copy.py` (already imports `parse_answer` and tests `activity_category`).

```python
def test_parse_answer_normalizes_activity_modifier_suffix() -> None:
    assert parse_answer("activity_category", "보통 수준") == "moderate"
```

Currently `parse_answer("activity_category", "보통 수준")` returns `"보통 수준"` (not `"moderate"`), so the assertion fails → RED. The fix belongs in the activity branch of `parse_answer` (copy.py:165-179) — normalize `"보통 수준"` (and analogous suffix forms) to their canonical category, mirroring the `"활동량입니다."` alias, while keeping deliberately-ambiguous free text (e.g. `"보통 운동을 자주 합니다"`, asserted at test line 39) un-normalized so it still re-asks.

<results>
<files>
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_copy.py - ROOT CAUSE. `parse_answer` activity branch (lines 165-179) normalizes exact `_ACTIVITY_CATEGORIES` tokens and the single "활동량입니다." alias only, returning the raw Korean string for natural modifiers like "보통 수준" (final `return ... else value` at line 179).
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_copy.py:15 - `_ACTIVITY_CATEGORIES` maps only the exact token "보통" (not "보통 수준").
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_copy.py:115-127 - `parse_legacy_activity_answer` only matches the "활동 수준: <label>. " prefix, not "수준" suffixes.
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_runtime_publication.py:75,94-106 - Production attestation calls `compile_clarification_policy`; if it yields issues, re-asks the exact-choices clarification.
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_preview_completion.py:122,121-134 - Preview uses the same deterministic policy to emit clarifications.
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_runtime_collection.py:250 (and 189) - Production collection stores the raw `parse_answer` result via `submit_answer`.
- /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_preview_flow.py:92 - Preview collection stores the raw `parse_answer` result.
- /home/cube/projects/richard/hermes-agent/tests/gateway/test_telegram_nutrition_onboarding_copy.py - Exact test seam for the failing regression test (already imports `parse_answer`, tests activity_category at lines 30-43).
- /home/cube/projects/richard/hermes-agent/tests/gateway/test_nutrition_onboarding_clarification_policy.py - Proves a correctly parsed ("moderate") activity value produces no issues/holds (`test_exact_natural_korean_fixture_compiles_without_issues_or_holds`), refuting hypothesis (3).
</files>

<answer>
The root cause is hypothesis (2): field-level parsing fails to normalize natural modifiers, which is a concrete case of (1). `parse_answer("activity_category", "보통 수준")` in `gateway/platforms/telegram_nutrition_onboarding_copy.py:165-179` only recognizes the exact token "보통" and the hardcoded alias "보통 활동량입니다.", so "보통 수준" is returned raw and stored verbatim. At attestation both production (`telegram_nutrition_onboarding_runtime_publication.py:75`) and preview (`telegram_nutrition_onboarding_preview_completion.py:122`) feed the stored answers into the deterministic `compile_clarification_policy` (installed `checkin_cli`), which flags the non-canonical activity value as ambiguous and re-asks for the exact choice. "보통" passes only because `parse_answer` normalizes it to "moderate", which the policy accepts. Hypothesis (3) is refuted: the policy does not re-ask valid enum values (proven by `test_exact_natural_korean_fixture_compiles_without_issues_or_holds`, which yields issues==[] for "moderate").
</answer>

<next_steps>
Add the regression test to `tests/gateway/test_telegram_nutrition_onboarding_copy.py`: `assert parse_answer("activity_category", "보통 수준") == "moderate"` (fails now, returns "보통 수준"). Then fix the activity branch in `telegram_nutrition_onboarding_copy.py:165-179` to normalize benign suffix forms (e.g. "X 수준", "X입니다", "X이에요") via a strip-and-match against `_ACTIVITY_CATEGORIES`, keeping deliberately-ambiguous free text (per existing test line 39) un-normalized.
</next_steps>
</results>


I read this as: find the exact Task26-certified way to run `test_nutrition_onboarding_clarification_policy.py` against the installed candidate wheel (venv, HERMES_HOME, PYTHONPATH), read-only.


Found the evidence directory. Let me read the installed-provenance binding and the running logs to extract the exact command.


Let me inspect the receipts directory, deployment dir, and the tail of the logs to recover the exact command and interpreter.


The correct run uses `tools/run_gateway_parallel.py` ("candidate interpreter, PYTHONPATH unset"). Let me read that harness and the independent verifier to pin the interpreter and invocation


The harness requires `HERMES_TEST_REPO_ROOT` and runs `sys.executable` (the candidate venv; PYTHONPATH unset). Let me find the exact invocation that produced the final passing run.


Let me find the exact invocation command in the session (the bash call preceding the isolated run).


The literal runner invocation isn't in the truncated lines. Let me search the session for the runtime venv python path and the parallel-runner command


Key command pattern confirmed at session line 99 (candidate venv python, PYTHONPATH unset). Let me confirm the exact runner invocation and the import-origin evidence for `checkin_cli` resolving from the candidate venv.


Let me read the exact bash invocation that launched the isolated gateway suite (session around the run


Let me confirm where the runner lives in the hermes repo and whether a focused single-file command for the policy test was recorded.


I have the full picture. Let me confirm the manifest binding block for the authoritative environment record.


The manifest confirms every knob. Let me read the full binding config to cite it precisely


I read this as: surface the exact Task26-installed-candidate command (venv/pythonpath/HERMES repo root) that makes `checkin_cli.nutrition_onboarding_clarification_policy` importable — read-only.

## Command (run the policy test against the installed candidate)

```bash
env -u PYTHONPATH \
  HERMES_TEST_REPO_ROOT=/home/cube/projects/richard/hermes-agent \
  /home/cube/.cache/task26-clarification-root-runtime-st_01a013d9/venv/bin/python \
  "/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-clarification-root-final-st_01a013d9/tools/run_gateway_parallel.py" \
  -j 12 tests/gateway/test_nutrition_onboarding_clarification_policy.py
```

Task26 actually ran it as part of the full fresh-interpreter-per-file gateway suite (same env, `HERMES_TEST_PATHS=tests/gateway`, 374 files / 12 workers); the policy-test file itself passed **14✓** in that run. The single-file form above is the equivalent repository-supported invocation via the same runner.

## Required environment (why your `uv run` variant fails)

- **No `uv`/hermes env.** Your `uv --directory hermes-agent run pytest ...` installs hermes but not `checkin_cli`, so the import is absent. `checkin_cli` and `gateway` are both wheels installed into the **candidate runtime venv**:
  - venv/interpreter: `/home/cube/.cache/task26-clarification-root-runtime-st_01a013d9/venv` (`venv/bin/python`)
  - site-packages: `/home/cube/.cache/task26-clarification-root-runtime-st_01a013d9/venv/lib/python3.12/site-packages` — this is where `checkin_cli.nutrition_onboarding_clarification_policy` resolves.
- **`PYTHONPATH` must be `unset`** (`pythonpath_unset: true`). Do NOT point it at the profile source `/home/cube/.cache/task26-strict-successor-1786976146/src-p`; that is `profile_source_metadata_only` and only leaks source instead of the installed candidate. With PYTHONPATH unset, `import-origins.json` proves `checkin_cli` resolves to the venv site-packages.
- **HERMES repo root**: `HERMES_TEST_REPO_ROOT=/home/cube/projects/richard/hermes-agent` (required by the runner; each per-file pytest subprocess runs with `cwd` = this root via `sys.executable`).
- Discovery root: `tests/gateway` (or the explicit test file); 12 workers (`gateway_workers: 12`), fresh interpreter per test file, bytecode writes disabled (`PYTHONDONTWRITEBYTECODE=1`), offline.

## Evidence sources

- `…/.omo/evidence/task26/task26-clarification-root-final-st_01a013d9/candidate-manifest.json` — binding.config: `venv`, `site_packages`, `pythonpath_unset:true`, `profile_source_metadata_only`, `gateway_runner_mode:fresh-interpreter-per-test-file`, `gateway_workers:12`, `hermes_source_tests`; gates `gateway_full: 8282 passed, 59 skipped`, `focused_regressions: 76 passed`.
- `…/receipts/import-origins.json` — `python: …/venv/bin/python`, `pythonpath: null`, `checkin_cli → …/venv/lib/python3.12/site-packages/checkin_cli/__init__.py`.
- `…/receipts/full-gateway-final.log` (and `full-gateway-isolated.log`) — header "per-file isolated complete Gateway suite …, candidate interpreter, PYTHONPATH unset"; policy-test file line `✓ tests/gateway/test_nutrition_onboarding_clarification_policy.py (14✓)`.
- `…/receipts/environment.txt` — canonical roots: `hermes`, `profile`, `runtime`, `evidence`.
- `…/tools/run_gateway_parallel.py` — the runner: reads `HERMES_TEST_REPO_ROOT`, runs `sys.executable -m pytest <file>` per file, PYTHONPATH untouched → must be unset at the call site.

No files or services were changed.