# RALPLAN Revision 9 — redacted, typed Korean expression postprocessor

## Decision and non-goals

This revision supersedes stages 05 and 07 and resolves every stage-08 blocker. It implements actual model-authored Korean composition in typed daily, weekly, and both live adaptive-card schema slots without giving the model semantic authority. The model returns a raw `generated_ko` string rather than a variant identifier or a code-selected wrapper. Every truth-bearing predicate, value, date, decision, action, safety condition, approval/delivery state, identity, button, heading, and layout byte remains issued and rendered locally.

The feature is presentation-only, disabled by default, and never activates a customer, changes consent, proposal calculation, adaptive epoch, delivery authority, Gate-D, or Telegram routing. It has one direct OpenAI-compatible identity, one non-streaming request at most per artifact, `max_retries=0`, no application retry, no alternate provider, and no use or change of `agent.auxiliary_client`, Kimi/Moonshot, Anthropic, Codex, OAuth, proxy, pool, or generic provider code. Failure always selects the already-rendered canonical body; it is never repaired or partially merged.

This is intentionally not an unrestricted body rewriter. An unrestricted prose response cannot be proven not to add a fact, reverse modality, or introduce a medical/action/delivery claim. The model instead composes raw Korean **style grammar** around code-issued semantic/action placeholders. The accepted language is not a finite local `variant_id`, lead table, or enumerated output catalog: the raw string is model-authored and is parsed as a compositional, recursive typed language. Its operational character/lexeme cap prevents resource abuse; it is not a finite list of locally rendered variants. All declarative material is still the exact local expansion of issued placeholders.

## Verified source boundaries

- Hermes implementation/test root: `/home/cube/projects/richard/hermes-agent`.
- Operator guide: `/home/cube/projects/richard/traning coach/듀얼코치_사용설명서.md`. The root `PILOT_RUNBOOK.md` is only a pointer; the pinned profile runbook remains unchanged.
- Profile roots: `/home/cube/.hermes/profiles/{physique-coach,dualcoachtest}/workspace/checkin_cli`.
- `TelegramAdapter._nutrition_daily_text(snapshot, feedback)` currently carries arbitrary generated feedback through `_nutrition_daily_interpretation`, `_saved_physique_coaching_feedback`, `_generate_physique_coaching_feedback`, `_request_physique_coaching_feedback`, and `_render_physique_feedback_replay` (`gateway/platforms/telegram.py:5629-5948,5878-6113`). That is the defect: a model receives semantic authority after canonical rendering.
- `_send_nutrition_coaching_tick` renders the weekly report before its `template_digest` and `reserve_customer_task_delivery` call (`telegram.py:6359-6603`). A `ScheduledDeliveryReceipt` intentionally exposes only digests, while its validated prepared ledger row stores the pinned body and destination (`customer_schedule.py:89-130,1245-1435` in both profiles).
- Fresh `menu|card` payloads are persisted by `AdaptiveOperatorService.mark_publish_pending` from `TelegramAdapter._handle_adaptive_review_callback`; `recover_pending_cards` republishes the persisted `card_payload` (`telegram.py:7209-7373`, `nutrition_coaching.py:3677-3757`). A selected adaptive presentation must therefore be validated and pinned before publication; recovery must not regenerate it.
- The two source card grammars are different and both are in scope. Physique-coach renders structured customer/day/D+, `결정:`, `근거:`, optional target/cycle/meal/note lines, and `상태: 운영자 승인 전 · 고객에게 자동 전달하지 않음` (`physique-coach/.../adaptive_nutrition.py:4649-4688`). Dualcoachtest renders `현재 판단:`, `권장안`, optional `검토 필요`, optional `상세 근거`, and `고객에게는 아직 전달되지 않았습니다.` (`dualcoachtest/.../adaptive_nutrition.py:4877-4916`). Hermes gateway adaptive tests import physique-coach, so dualcoachtest requires its own profile-root golden/test invocation.
- `PhysiqueCheckinConfig.from_extra` currently makes `coaching_feedback_enabled` part of the check-in config (`physique_checkin_config.py:8-50`). Both current profile configs contain that legacy key; physique-coach has its `physique_checkin.enabled: true` runtime shape. Removing/rejecting the key without an ordered migration could silently disable the active check-in.
- Hermes pins `openai==2.24.0` (`pyproject.toml:45`).

## Canonical surfaces, exact insertion points, and recovery

| Surface/version | Canonical source and only eligible slot | Hard-locked material | Durable selected-body owner |
|---|---|---|---|
| `daily.v1` | `_nutrition_daily_text(snapshot)` status paragraph between fact bullets and `오늘 할 일` | title, seven fact lines, headings, blank lines, actions/order, all customer values | none; initial save edit only |
| `weekly.v1` | `_nutrition_report_text` deterministic interpretation and rationale paragraphs, only when both come from typed fallback branches | report title, all facts/ranges/rates/judgment/action bullets/layout, and any explicit unstructured source copy | existing scheduled-delivery `body` |
| `adaptive.dualcoachtest.card.v1` | fresh enveloped `status == "card"`: full `현재 판단:` line and each code-mapped `검토 필요` reason line | title/envelope/customer/date/facts/goal/recommendations/note/delivery line/buttons | existing adaptive `card_payload` |
| `adaptive.physique.card.v1` | fresh enveloped card: new code-owned `검토 안내` sentence immediately before final status | every existing source line, heading, final status, envelope/buttons | existing adaptive `card_payload` |

### Daily cutover

Refactor `_nutrition_daily_text` to accept only `snapshot`; it derives `missing` from the existing seven rendered facts. Its canonical status is exactly:

- complete: `저장된 오늘 기록을 확인했습니다.`
- incomplete: `일부 항목이 기록되지 않아 저장된 내용만 안내합니다.`

Delete the old feedback methods and field route: `_saved_physique_coaching_feedback`, `_generate_physique_coaching_feedback`, `_request_physique_coaching_feedback`, and model-derived `_nutrition_daily_interpretation(feedback)` must not remain reachable from a finalized daily render. Active-turn and conversation helpers are separate features and retain their present behavior.

Add `PhysiqueCheckinBridge.finalized_coaching_artifact(session_id) -> FinalizedCoachingArtifact | None`, and equivalent active/latest accessors. Each loads one validated finalized session once and returns the snapshot plus its `finalized_event_id` from that same session load. The event ID is internal only and is used to derive the opaque artifact key. First save renders canonical, optionally obtains a candidate, and performs one edit. `_render_physique_feedback_replay` is canonical-only: it never opens a claim, calls a provider, or recreates an old selected expression.

### Weekly pin-before-send and restart

For a new weekly task: canonical report → eligible postprocess candidate or canonical → exact `template_digest` → `reserve_customer_task_delivery(final_body, ...)` → existing transition/send authority. The postprocessor never runs after reservation.

Add the same narrow API and exported dataclass to **both** profile copies of `checkin_cli/customer_schedule.py` and `checkin_cli/__init__.py`:

```python
@dataclass(frozen=True, slots=True)
class PinnedScheduledDelivery:
    receipt: ScheduledDeliveryReceipt
    body: str
    destination: object

def load_prepared_customer_task_delivery(
    profile_root: Path,
    receipt: ScheduledDeliveryReceipt,
) -> PinnedScheduledDelivery: ...
```

Under the existing schedule writer lock, this API validates the ready fence, full ledger hash chain, tombstone pairing, receipt/task identity, current state `prepared`, body/destination digests, and registry/config pins before returning the exact durable body/destination. It is read-only, is not a generic lookup, and adds no schedule row or schema.

At the top of the weekly task branch, an existing `prepared` receipt invokes this reader before any report rendering. The gateway revalidates live registry/config/destination pins, transitions the same receipt to `sending`, and sends the pinned bytes. It makes zero Korean-expression/OpenAI calls. A read or authority failure marks `unknown` with `prepared_material_unavailable` only if the validated ledger permits that append; if the ledger is corrupt enough to prevent it, it hard-stops without a provider call. `sending`, `delivered`, `unknown`, `abandoned`, and `sent_audited` retain their existing no-retry semantics.

### Adaptive schemas and publication boundary

The adapter accepts only a fresh `status == "card"` payload. `menu`, `view`, operator-input, terminal delivery, published, malformed, unknown future schema, and recovery payloads are canonical/no-call.

- **Dualcoachtest parser:** require the exact envelope separator, title prefix, one `현재 판단: ` line before `\n\n권장안\n\n`, and zero or more mapped review lines only in an optional `\n\n검토 필요\n\n` block ending at `\n\n상세 근거\n\n` or the final not-delivered line. Each extracted reason must map to a known fixed reason enum; otherwise the entire card is canonical/no-call.
- **Physique parser:** require its current ordered source grammar and final exact status. Its canonical presentation projection adds, immediately before status:

  ```text
  검토 안내

  제안의 구조화된 항목을 확인한 뒤 승인 여부를 결정하세요.

  ```

  Only the sentence is a slot. This is a code-owned presentation action, not a `NutritionProposal` field or delivery/approval state change.

Both adapters preserve all non-slot bytes exactly. A selected or canonical payload is pinned by the service operation below; recovery republishes `card_payload` unchanged with zero expression calls.

## Redacted semantic capsule and typed Korean contract

`gateway/platforms/korean_expression.py` is the only new feature module. It defines frozen/slotted `KoreanExpressionConfig`, `CanonicalSurface`, `ProseSlot`, `Placeholder`, `PresentationCandidate`, `KoreanExpressionCoordinator`, strict parser, pure per-schema adapters, direct client, and journal. It imports neither a profile-local `checkin_cli` package nor the auxiliary provider router.

The provider receives a fixed system instruction containing the enum glossary and a bounded JSON capsule. It receives **no** canonical body, raw fact/action expansion, customer key/label/name, chat/topic/session/event/schedule/proposal ID, note, address, date, measurement, token, digest of customer text, history, or telemetry. `artifact_key` is a random-key HMAC and is not reversible.

```json
{
  "schema_version":"korean-expression-v4",
  "surface":"weekly.v1",
  "artifact_key":"opaque-hmac-hex",
  "slots":[{
    "slot_id":"weekly.interpretation",
    "allowed_predicate_ids":["P_WEEKLY_IN_GOAL","P_WEEKLY_KEEP_BASIS"],
    "allowed_action_ids":[],
    "required_placeholder_tokens":["[[P:01]]","[[P:02]]"],
    "max_generated_chars":160,
    "max_style_lexemes":12
  }]
}
```

The model returns one UTF-8 JSON object (16 KiB maximum) with exactly `schema_version`, `surface`, `artifact_key`, and `slots`. A slot has exactly `slot_id`, `predicate_ids`, `action_ids`, `placeholder_tokens`, and `generated_ko`. It must echo the issued ID/token arrays exactly in order and its open Korean `generated_ko` must contain the issued literal placeholder tokens. It has no `variant_id`, no full body, offsets, tool call, prompt echo, semantic text field, or extra key.

`[[P:NN]]` and `[[A:NN]]` are immutable, slot-local placeholders. The model sees IDs/tokens and the static enum glossary but never their expansion. Locally, each placeholder has a single NFC exact expansion decided before the call. It may hold a value only locally; current v1 slots deliberately keep values/dates outside slots, so their expansions are fixed code-owned propositions/actions. Replacing a placeholder is performed only after every validation succeeds.

### Per-slot semantic manifest

| Slot | Allowed predicate IDs / action IDs | Required placeholders in order | Max chars / style lexemes | Ineligible conditions |
|---|---|---|---|---|
| `daily.status` | exactly one of `P_DAILY_COMPLETE`, `P_DAILY_PARTIAL`; actions `[]` | `[[P:01]]` | 96 / 8 | malformed/finalized snapshot unavailable |
| `weekly.interpretation` | one exact typed branch: `{P_WEEKLY_TREND_UNAVAILABLE,A_WEEKLY_RECHECK_AFTER_RECORD}`, `{P_WEEKLY_TREND_OBSERVED,A_WEEKLY_HOLD_FOR_MISSING_GOAL}`, `{P_WEEKLY_IN_GOAL,P_WEEKLY_KEEP_BASIS}`, or `{P_WEEKLY_OUT_OF_GOAL,A_WEEKLY_RECHECK_INPUT}` | one token per ID in canonical branch order | 220 / 16 | any explicit `interpretation`/`trend_interpretation`/`summary_text`, untyped field, or unsupported report branch |
| `weekly.rationale` | exactly one of `A_WEEKLY_KEEP_BASELINE`, `A_WEEKLY_REVIEW_ADJUSTMENT`, `P_WEEKLY_RECORDS_INSUFFICIENT`; no additional action | `[[A:01]]` or `[[P:01]]` | 144 / 10 | explicit `rationale`/`judgment_rationale`/`reason`, or unsupported judgment |
| `adaptive.dual.judgment` | one `P_DUAL_DECISION_<known Decision enum>`; actions `[]` | `[[P:01]]` | 128 / 10 | card grammar/envelope/session/proposal/revision mismatch |
| `adaptive.dual.review.N` | one `P_DUAL_REASON_<known reason enum>`; actions `[]` | `[[P:01]]` | 128 / 10 | unknown/missing/reordered reason or review block mismatch |
| `adaptive.physique.review_instruction` | predicates `[]`; exactly `A_OPERATOR_REVIEW_DECISION` | `[[A:01]]` | 128 / 10 | exact current physique card grammar/status mismatch |

The static enum glossary is code reviewed. Examples of local expansions are `P_DAILY_COMPLETE → 저장된 오늘 기록을 확인했습니다`, `P_WEEKLY_IN_GOAL → 체중은 린매스업 목표 범위 안에서 안정적으로 증가했습니다`, and `A_OPERATOR_REVIEW_DECISION → 제안의 구조화된 항목을 확인한 뒤 승인 여부를 결정하세요`. The glossary and IDs are generic semantic types, not customer fields. A typed branch is computed from the same local facts used for the canonical renderer; it is never inferred from model text.

A complete redacted accepted vector is therefore possible without sending the fact:

```json
{"slot_id":"weekly.interpretation","predicate_ids":["P_WEEKLY_IN_GOAL","P_WEEKLY_KEEP_BASIS"],"action_ids":[],"placeholder_tokens":["[[P:01]]","[[P:02]]"],"generated_ko":"차분히, [[P:01]]. 이어서 [[P:02]]."}
```

The result is model-authored Korean and differs from canonical text, while the two truth-bearing sentences are expanded only from local immutable tokens. Corresponding valid daily, dual, and physique examples are `오늘은 차분히, [[P:01]].`, `우선, [[P:01]].`, and `차분히, [[A:01]].`. These are examples, not accepted local variants.

### Korean style grammar, prohibited claims, and deterministic rejection

No free segment may be a proposition. `generated_ko` uses this grammar, where `TOKEN` is only an issued placeholder and `STYLE` is a model-composed sequence, not a code-selected wrapper:

```text
PROSE     ::= STYLE? TOKEN (". " STYLE? TOKEN){0,2} "."
STYLE     ::= LEXEME (" " LEXEME){0,MAX_STYLE_LEXEMES-1} [", "]
LEXEME    ::= one exact item from SAFE_STYLE_LEXEMES
TOKEN     ::= issued [[P:NN]] | issued [[A:NN]]
```

`SAFE_STYLE_LEXEMES` is a static code constant, not prompt-only policy:

```text
차분히 천천히 가볍게 우선 먼저 이어서 또 그리고 하나씩 순서대로
함께 지금 이제 여기서 아래 바로 그대로 특히 잠시 다시
```

Only ASCII space, comma, and period are allowed around these lexemes. The grammar permits placement and composition of up to 16 independently model-chosen style lexemes around one to three typed propositions/actions; it has no `LEAD`, `JOIN`, whole-output table, or selector. The validator requires at least one style lexeme, so a pure placeholder/canonical response is rejected rather than counted as selected. Any proposal to expand the lexicon, maximums, or semantic inventory requires code/golden review.

For all slots, the following are forbidden outside placeholders: every Hangul/Latin token not in the exact lexicon; digits; Hangul number words; `+`, `-`, `%`, `~`; all date/unit strings (`kg`, `kcal`, `g`, `L`, `일`, `주`, `월`, `년`, `시간`); quotes; newline/tab/control/BOM; alternate/nested brackets; English; emoji; URLs; names; and all headers/bullet syntax. The feature also explicitly rejects evidential claims such as `확인 결과`, `기록상`, `분석`, and any evaluation/completeness/health/medical/safety/delivery/approval/action vocabulary including `잘`, `충분`, `성공`, `좋`, `안전`, `위험`, `진단`, `치료`, `약`, `유지`, `조정`, `측정`, `기록`, `승인`, `전달`, `전송`, `보내` outside an issued token.

Per-slot additional prohibitions are: daily cannot assert quality/adherence/health; weekly cannot add a cause, risk, target, fact, or action; dual cannot alter decision/recommendation/hold/approval/delivery; physique cannot assert that review/approval already happened, medical safety, or a new action. Thus `확인 결과, [[A:01]].` is invalid everywhere: it smuggles an unsupported completed-verification proposition.

The parser rejects invalid UTF-8, BOM/control/NFC failure, duplicate keys at every depth, non-exact key sets, booleans/type confusion, wrong/reordered/missing/extra slots or IDs, wrong artifact key, missing/duplicate/cross-slot/reordered/new tokens, token literal expansion, raw numbers/dates/units, a token/lexeme/grammar violation, canonical-only selector, length/lexeme limit, or any output differing in a locked byte. One failed slot rejects the entire document; there is no repair, extraction, retry, or partial selection. After local expansion it reconstructs the complete body and asserts byte identity outside declared boundaries, including Telegram UTF-16 bounds, envelope, headings, blank lines, fact/action order, and buttons.

## Single configured OpenAI-compatible provider

`KoreanExpressionConfig.from_extra` alone parses `config.extra["korean_expression_postprocessor"]` at `TelegramAdapter.__init__`. Missing config is disabled. A configured identity must have this exact shape and equality; all unknown/missing/trimmed/normalized fields fail closed:

```yaml
korean_expression_postprocessor:
  schema_version: korean-expression-v4
  mode: disabled # disabled | shadow | enabled
  surfaces:
    daily: false
    weekly: false
    adaptive_operator: false
  timeout_seconds: 3
  provider:
    provider_id: openai-direct
    transport: chat_completions
    endpoint: https://api.openai.com/v1
    model: gpt-4.1-mini
```

The compiled positive allow-list is exactly `(openai-direct, chat_completions, https://api.openai.com/v1, gpt-4.1-mini)`. `OPENAI_API_KEY` is the only credential read, only at direct-client construction; config secrets, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, provider/model defaults, OAuth/session credentials, pools, and auxiliary credentials are ignored and cannot override it. Missing/whitespace key is `credential_unavailable` with zero HTTP calls. Invalid configuration is disabled with zero decision/client/request; it is not a persisted `provider_disallowed` state.

Construct only:

```python
openai.OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    base_url="https://api.openai.com/v1",
    max_retries=0,
    timeout=3.0,
)
```

Make exactly one non-streaming `chat.completions.create` call with the literal model, `n=1`, `temperature=0.7`, `max_tokens=384`, `response_format={"type":"json_object"}`, `store=False`, no tools, static system instruction, and the redacted capsule as the only user message. The worker deadline is four seconds. A 429, 5xx, transport error, SDK timeout, cancellation, malformed response, or late result makes no second request. A late worker reacquires the journal lock, observes that it no longer owns an unexpired start row, discards the result, and cannot render/persist/publish it.

## Outcome journal: schema, atomicity, states, and retention

There is no feature-owned store of customer-visible text. Selected weekly bytes live only in the existing scheduled ledger; selected adaptive bytes live only in the existing pending card; daily has no selected replay. `data/korean-expression-v1/` contains only a random 32-byte `identity.key`, lock, and metadata outcome journal. The root is 0700; key/lock/journal files are 0600, no-follow, current-UID owned, and read back after creation. Bad mode, symlink, inode replacement, malformed row, torn write, or hash-chain failure returns canonical with zero new call.

`artifact_key = HMAC-SHA256(identity.key, b"korean-expression-v4\\0" || length-prefixed UTF-8 NFC fields)`. Inputs are `(surface, finalized_event_id)` for daily, `(surface, schedule_key, kind, kst_day)` for weekly, and `(surface version, session_id, proposal_digest, revision, card kind)` for adaptive. Raw inputs never leave process and are never journaled, logged, emitted, or sent to the provider.

Records are daily UTC segment files `outcomes/YYYY-MM-DD.jsonl`; an attempt and its terminal row use the segment selected by the start timestamp even across midnight. This avoids unsafe compaction while making retention precise. Each line is NFC UTF-8 canonical JSON with sorted keys, `ensure_ascii=false`, separators `(',', ':')`, and one LF, containing exactly:

```text
schema_version, sequence, artifact_key, surface, mode, attempt_id, state,
outcome, started_at_utc, deadline_at_utc, finalized_at_utc,
previous_row_digest, row_digest
```

- `schema_version` is `korean-expression-journal-v1`; `sequence` starts at 1 in each segment and is contiguous.
- `artifact_key`, `attempt_id`, and digests are lower-case hex (`64`, `32`, and `64` characters respectively).
- First `previous_row_digest` is exactly 64 zeroes; every later row equals the immediately preceding full `row_digest` in that segment.
- `row_digest` is SHA-256 of the UTF-8 canonical JSON of that row with `row_digest` omitted.
- Timestamps are exact RFC3339 UTC `YYYY-MM-DDTHH:MM:SS.ffffffZ`; start/deadline are copied verbatim into terminal rows; `finalized_at_utc` is `null` only for a start row.
- Start row is exactly `state=attempt_started`, `outcome=attempt_started`. Terminal row is exactly `state=finalized` and one of `selected`, `shadow_selected`, `credential_unavailable`, `provider_error`, `timeout`, `invalid_response`, `validation_rejected`, `commit_failed`, `publication_failed`, or `claim_expired_canonical`.
- `mode` is `shadow|enabled`; disabled and invalid config produce no rows.

Under the journal flock, the owner appends and `fsync`s `attempt_started` before its only wire request. An observer of an unexpired attempt returns `in_flight` in memory with `publication_permitted=false`, writes no terminal row, does not send, and does not call the provider. A restart/observer after expiry appends `claim_expired_canonical`, makes no call, and proceeds only with canonical output. This preserves a healthy owner and prevents a second attempt. Weekly/adaptive terminal `selected` is appended only after their existing durable body/card write; daily after the Telegram edit. Failure of those downstream operations uses `commit_failed`/`publication_failed` and publishes no unpinned selected text.

Under the same journal lock a segment is retained for exactly 35 completed UTC calendar days plus the current day. On startup/before a new attempt, a validated segment older than that is removed as a whole and its directory is fsynced. Active claims cannot be that old; an old incomplete claim is first terminalized `claim_expired_canonical`, then eligible for normal retention. This metadata has no raw customer data. Independently, no normal product ingress may call the model for an old identity: daily replay is canonical-only, weekly existing rows are pinned/no-call, and adaptive recovery/terminal states are pass-through/no-call. Retention therefore cannot reopen a customer-visible retry path.

Telemetry is injectable/nonblocking and exactly `{event, contract_version, surface, mode, outcome, latency_bucket}`. It contains no text, prompt/response, placeholder/semantic ID, artifact key, raw ID, values, exception message, provider/model, destination, or length. Sink failure is ignored.

## Adaptive validation and persistence are one locked service operation

Replace direct gateway use of public `mark_publish_pending` for a fresh card with this exact API in `AdaptiveOperatorService`:

```python
def validate_and_mark_presented_card(
    self,
    callback_data: object,
    *,
    canonical_payload: Mapping[str, object],
    candidate: PresentationCandidate,  # selected or canonical, internal only
    origin_message_id: object = "",
) -> Mapping[str, object]: ...
```

`PresentationCandidate` holds only in-process `surface_version`, `attempt_id`, `artifact_key`, `binding_digest`, and already strict-parsed slot composition/rendered text. `binding_digest` is an internal SHA-256 over canonical JSON of the fresh callback/session action, proposal digest, revision, enveloped canonical payload text/buttons, origin provenance, and source schema version. It is neither persisted nor exposed to the provider/user/telemetry.

The service acquires its existing `_authority_session_lock()` and `_publication_ledger_lock()` once. While both are held it:

1. reloads the latest session and rechecks callback action, expiry, exact operator triple, canonical owner/config/registry/consent/activation/source/epoch pins, provenance/origin message, proposal digest/revision, and fresh `status == "card"`;
2. reparses the supplied canonical payload with the versioned adapter, recomputes `binding_digest`, and requires equality with the candidate;
3. reruns pure slot/render validation against that newly parsed canonical surface, so only declared slot bytes can differ;
4. calls extracted private `_mark_publish_pending_unlocked` to validate/persist that exact payload in the same critical section.

Only after this returns may `TelegramAdapter` edit the Telegram card and later call existing `mark_published`. `menu` remains on its current canonical persistence path. A stale/invalid candidate or any authority/revision/owner change produces no `publish_pending` row and no edit; the coordinator terminalizes `commit_failed` and makes no new expression call. A malformed candidate with still-current authority is converted to canonical before this operation, so canonical persistence remains available without accepting untrusted text.

The race test uses a barrier after candidate creation and before acquiring the service locks. It changes owner/config/epoch or replaces the proposal revision, then releases the candidate commit. It must prove `validate_and_mark_presented_card` raises, appends zero pending rows, invokes no editor, and leaves provider calls at one. A second test races two commits of the same candidate: exactly one immutable pending row results. Two unequal candidates for the same card produce one row and a deterministic publication conflict, never an overwrite. Recovery reads that one row and makes zero provider calls.

## Legacy configuration migration and rollback-safe deployment

The new mode is absent/disabled unless deliberately configured. It never derives enablement from `coaching_feedback_enabled`.

1. **Config-first migration:** before deploying parser removal, remove only `coaching_feedback_enabled` from both `/home/cube/.hermes/profiles/physique-coach/config.yaml` and `/home/cube/.hermes/profiles/dualcoachtest/config.yaml`; add the exact disabled `korean_expression_postprocessor` block above. Validate that physique-coach still parses a non-`None` enabled `PhysiqueCheckinConfig` and that canonical daily rendering remains available. Old code treats the absent legacy boolean as false, so this ordering disables only obsolete generated feedback, not the active check-in.
2. **Preflight gate:** a profile preflight reads both deployed config shapes and fails release explicitly if any loaded `physique_checkin` still contains `coaching_feedback_enabled`; it reports an enum diagnostic, never silently falls back to a disabled bridge. It also asserts the new postprocessor is disabled unless its exact full allow-listed config is intentionally present.
3. **Code cutover:** remove `coaching_feedback_enabled` from the dataclass and all daily feedback calls. After the config-first gate, `PhysiqueCheckinConfig.from_extra` rejects that obsolete supplied key as an invalid configuration, with a visible startup diagnostic. There is no hidden compatibility mode that could preserve the unsafe feedback route or enable the new postprocessor.
4. **Migration regression:** test the current active physique shape with key removed/new feature absent; it must initialize the bridge and produce canonical daily output. Test both current legacy shapes fail only at preflight with the explicit legacy diagnostic, not as a silent optional-feature bypass. Test new config missing, disabled, malformed, and each surface false all make zero requests.

Rollback is config-first: set `mode: disabled` and all surfaces false, stop the gateway/scheduler, wait longer than the four-second claim deadline, and restart. No new direct requests occur. Existing prepared weekly body and pending adaptive `card_payload` remain exact and continue through their existing recovery authorities; daily is canonical-only. Preserve journal/ledger/card evidence; do not truncate JSONL, retry an unknown delivery, or manually alter schedule/adaptive records. Restoring the previous code is safe because the legacy key remains absent, so it cannot re-enable raw feedback.

## File-level implementation sequence

1. Add Hermes `gateway/platforms/korean_expression.py` with the strict config, redacted semantic capsule, compositional grammar, pure adapters, direct one-wire client, HMAC identity/journal, and coordinator. No profile import and no provider-router change.
2. Modify Hermes `gateway/platforms/physique_checkin.py`, `physique_checkin_config.py`, and `telegram.py`: atomic finalized artifact accessor, daily cutover, weekly postprocess-before-reserve plus prepared reader branch, and fresh-card operation integration. Delete obsolete daily feedback route rather than leaving an alias.
3. Modify `gateway/platforms/nutrition_coaching.py` only to add `validate_and_mark_presented_card` and extract private unlocked `mark_publish_pending` logic. Do not modify proposal/event/delivery schemas or terminal recovery behavior.
4. Modify both profile `checkin_cli/customer_schedule.py` and `checkin_cli/__init__.py` for `PinnedScheduledDelivery`/`load_prepared_customer_task_delivery` only.
5. Update both real profile configs by the ordered migration and update only `듀얼코치_사용설명서.md` as non-executable guidance. The guide documents disabled default, exact allow-list/no retry, redacted capsule, no content telemetry, both adaptive versions, shadow gate, migration, and rollback; the pinned runbook is unchanged.
6. Preserve the stage-07 non-copy attribution in the guide: immutable links to `https://github.com/Gaeduck-0908/im-not-ai-kiro/tree/901e378ae7f77035d17a491067343ac3f83d0214` and `https://github.com/epoko77-ai/im-not-ai/tree/53e24e8f92cf344efcb812103f7c2b203e7efffc`, plus the exact statement that they were consulted only for a high-level preservation goal and no source code, prompts, rules, workflow, or artifacts were copied. No MIT notice is reproduced because no material is copied.

## Verification: goldens, fake provider, race, profiles, and controlled live shadow

Add `tests/gateway/test_korean_expression.py` with:

- daily complete/partial, typed weekly branch, `adaptive.dualcoachtest.card.v1`, and `adaptive.physique.card.v1` canonical/selected/fallback goldens; locked byte, envelope, button, and UTF-16 assertions;
- one complete redacted request/response/local-render vector per slot and a proof that selected `generated_ko` is raw composition, not a local variant;
- duplicate-key/UTF-8/BOM/NFC/control/type/key/order/token/number/date/unit/raw expansion/semantic ID/action ID/forbidden vocabulary/negation-modality/evidential/medical/approval-delivery/layout attacks, all deterministically canonical;
- journal fixed hash-chain vectors, first predecessor zero digest, timestamp/enum/state/transition/permission/symlink/torn segment/retention tests; no content persistence/logging;
- fake direct OpenAI tests for literal endpoint/model/request, credential-only policy, no auxiliary resolver, one call for timeout/429/5xx/network failure, zero retry, and zero client for every invalid/disallowed config;
- owner/observer/restart-before-expiry/restart-after-expiry/timeout-late-result traces, exactly one call and no competing publication;
- guide attribution/contract assertion in `test_operator_guide_korean_expression_contract`.

Extend Hermes seam tests:

- `tests/gateway/test_telegram_physique_checkin.py`: migration/preflight shapes, active bridge continuity, legacy deletion, daily selected/canonical/replay behavior, atomic artifact identity, and active-turn/conversation non-regression.
- `tests/gateway/test_nutrition_coaching.py` and `tests/gateway/test_telegram_physique_checkin.py`: weekly prepared-body reader, crash after prepared selected body, exact pinned send, no expression call/re-render, and one Telegram authority.
- `tests/gateway/test_adaptive_nutrition.py` and `tests/gateway/test_telegram_group_gating.py`: both schema adapter fixtures, fresh-card atomic commit race, candidate conflict, pending recovery zero call, and terminal/menu/view no-call.
- Both profile `tests/test_adaptive_nutrition.py`: source-renderer shape golden plus adapter accepted/canonical/no-call mapping. Both profile `tests/test_customer_schedule.py`: prepared material reader success, digest/config mismatch, corrupt ledger hard stop, and no append from reader.

Run from `/home/cube/projects/richard/hermes-agent`:

```text
pytest -q tests/gateway/test_korean_expression.py tests/gateway/test_telegram_physique_checkin.py tests/gateway/test_nutrition_coaching.py tests/gateway/test_adaptive_nutrition.py tests/gateway/test_telegram_group_gating.py
pytest -q /home/cube/.hermes/profiles/physique-coach/workspace/checkin_cli/tests/test_adaptive_nutrition.py /home/cube/.hermes/profiles/physique-coach/workspace/checkin_cli/tests/test_customer_schedule.py
pytest -q /home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/tests/test_adaptive_nutrition.py /home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/tests/test_customer_schedule.py
pytest -q tests/gateway/test_korean_expression.py::test_operator_guide_korean_expression_contract
```

Add non-default marker `live_korean_expression` and `tests/gateway/test_korean_expression_live_shadow.py`. It uses a temporary feature root, synthetic redacted manifests only, monkeypatches every Telegram sender/editor/reservation/mark-pending entrypoint to raise, captures logs/telemetry, and directly invokes only the direct OpenAI client. It runs four schema cases: `daily.v1`, `weekly.v1`, `adaptive.dualcoachtest.card.v1`, and `adaptive.physique.card.v1`.

```text
RUN_KOREAN_EXPRESSION_LIVE=1 pytest -q -m live_korean_expression tests/gateway/test_korean_expression_live_shadow.py
```

The controlled proof has exactly 12 distinct synthetic capsules per case (48 total calls) within one 24-hour release-candidate window. It passes only with 12 `shadow_selected` results for **each** of the four cases, zero timeout/provider/validation/credential outcomes, zero Telegram/persistence call, and zero content-bearing captured field. Both adaptive versions must pass independently before the shared `adaptive_operator` flag is eligible for enablement.

Promotion remains intentional and one surface at a time: configuration starts disabled; after normal suite and the 48/48 controlled proof, use test-profile `shadow` for 14 days and require 10/10 selected observations with zero failures for daily, 10/10 weekly, and 10/10 for each adaptive schema. Enable daily, then weekly, then shared adaptive only after both 10/10 adaptive evidence sets pass. Any failure leaves/reverts that surface disabled. This is not real-customer activation, Gate-D completion, or delivery approval.

## Acceptance gates

1. The old free-form daily feedback route is absent; default/disabled output is byte-identical canonical and active physique check-in remains available after migration.
2. A real model authors a raw non-selector Korean composition in every requested surface: daily, weekly, dualcoachtest adaptive, and physique-coach adaptive. All facts/actions/statuses remain exact local placeholder expansion.
3. Capsules contain only public semantic/action enum IDs, opaque HMAC key, slot policy, and placeholders—never identifiers, notes, raw sensitive values, facts, dates, or measurements.
4. Per-slot ID/token/order/vocabulary/number/date/length/change rules reject deterministically; no partial output, retry, or fallback provider exists.
5. Adaptive presentation validation and pending-card persistence occur in `validate_and_mark_presented_card` under the same service locks; the authority-change interleaving test proves no stale card is published.
6. Weekly restart sends the exact prepared bytes through existing delivery authority with zero model call; adaptive recovery reuses exact persisted payload; daily replay is canonical-only.
7. Both profile adaptive and schedule suites, goldens, fake direct-provider tests, atomic race tests, exact journal-chain tests, and the four-case 48/48 controlled real-model shadow proof pass before any enablement.
8. Rollback disables only new requests while retaining existing durable delivery/card bytes and append-only evidence.
