# RALPLAN Revision 7 — guarded real Korean LLM postprocessor

## Status, fallback provenance, and non-goals

This full revision supersedes stage 05 where it conflicts and resolves all stage-06 Architect/Critic findings for run `019fa7fb-3be3-7000-b23f-03f6c05d81ff`. The prior planner context is unavailable; fallback metadata is `planner_id=0-HumanizerFinalRevision`, `planner_resumable=true`, `fallback_reason=context_unavailable`, `fallback_attempted_id=335-HumanizerIntentRevision`, `fallback_stage_n=7`.

**Goal:** a real LLM writes natural Korean in small typed slots of final canonical daily, weekly, and adaptive-operator strings. It is not a finite local variant selector: the model returns a raw Korean JSON composition and no `variant_id` is accepted. Code is the only authority for facts, values, calculations, decisions, safety, actions, approval/delivery state, identities, layout, buttons, and canonical fallback.

Default is disabled. The feature is presentation-only and bounded: no customer activation/consent/Gate-D changes, no proposal/delivery schema/state-machine changes, no broad provider refactor, no `agent.auxiliary_client` changes/calls, no Kimi/Moonshot/Codex/Anthropic, no encrypted or feature-owned presentation persistence, no retry, and no second Telegram delivery path. This planning execution used the permitted OpenAI fallback route only; no Kimi agent or execution path was used.

## Verified source seams and roots

- Product checkout: `/home/cube/projects/richard/hermes-agent`; guidance: `/home/cube/projects/richard/traning coach/듀얼코치_사용설명서.md`; profiles: `/home/cube/.hermes/profiles/{physique-coach,dualcoachtest}/workspace/checkin_cli`.
- `TelegramAdapter._nutrition_daily_text(snapshot, feedback)` and `_saved_physique_coaching_feedback` / `_generate_physique_coaching_feedback` / `_request_physique_coaching_feedback` / `_render_physique_feedback_replay` still admit arbitrary feedback (`gateway/platforms/telegram.py:5629-5948,6074-6113`). `coaching_feedback_enabled` is declared/parsed in `physique_checkin_config.py:8-50`.
- Weekly rendering is before `template_digest` and `reserve_customer_task_delivery` (`telegram.py:6359-6578`). `ScheduledDeliveryReceipt` exposes only digests, while prepared ledger rows contain actual body/destination (`customer_schedule.py:89-130,1245-1279`).
- Fresh adaptive card payload is persisted by `mark_publish_pending`; recovery republishes that payload (`telegram.py:7225-7355`, `nutrition_coaching.py:3677-3757`).
- The active Hermes adaptive suite imports physique-coach (`tests/gateway/test_adaptive_nutrition.py:1-45`). Its renderer has structured `결정`/`근거` plus status (`physique-coach/.../adaptive_nutrition.py:4649-4688`); dualcoachtest has `현재 판단`/`검토 필요` prose (`dualcoachtest/.../adaptive_nutrition.py:4877-4916`).
- Hermes pins `openai==2.24.0` (`pyproject.toml:45`). `agent.auxiliary_client` intentionally performs broad resolution/normalization and is excluded.

**Root cause:** domain rendering is deterministic but the old daily path grants post-render free text semantic authority. The replacement is a gateway-only, typed semantic-anchor postprocessor over final canonical strings.

## Final canonical surfaces and publication boundary

Every candidate starts from a final canonical string. The adapter returns exact locked fragments and slots; reconstruction must retain every locked byte. Unknown/malformed shape, disabled/ineligible state, unavailable credential, invalid metadata, in-flight loser, request failure, or response rejection returns canonical.

| Surface | Final canonical source | LLM slot(s) | Existing durable final-body owner |
|---|---|---|---|
| daily | `_nutrition_daily_text(snapshot)` | `daily.status` | none; selected text exists only for the first successful Telegram edit |
| weekly | `_nutrition_report_text(...)` before digest/reservation | `weekly.interpretation`, `weekly.rationale` | scheduled-delivery `body` via `reserve_customer_task_delivery` |
| adaptive dualcoachtest | fresh enveloped `status == "card"` payload | entire current-judgment line; every review-reason line | adaptive `card_payload` via `mark_publish_pending` |
| adaptive physique-coach | fresh enveloped `status == "card"` payload | one gateway-only presentation instruction sentence | adaptive `card_payload` via `mark_publish_pending` |

### Daily

`_nutrition_daily_text` takes only `snapshot`, derives missingness from the seven existing facts, and freezes:

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

Title, facts, headings, blank lines, action text/order, and values are locked. No slot may praise, claim quality/adherence/health, or introduce a medical/safety assertion. Remove the old feedback methods and field; reject a supplied legacy `coaching_feedback_enabled` key. `PhysiqueCheckinBridge` supplies one atomic `(snapshot, finalized_event_id)` for initial render. The initial save edit may use one validated candidate; `_render_physique_feedback_replay` is canonical-only and never opens a claim or provider call.

### Weekly, including prepared restart

For a new weekly task: render canonical → guarded postprocess → calculate digest → `reserve_customer_task_delivery(selected_or_canonical_body)` → existing send authority. No LLM runs after reservation.

Add one narrow read API to **both** profile copies and export it in each `checkin_cli/__init__.py`:

`load_prepared_customer_task_delivery(profile_root, receipt) -> PinnedScheduledDelivery(body, destination)`.

It accepts only a `ScheduledDeliveryReceipt`; under the existing schedule lock it validates fence, ledger/hash chain, current prepared row, receipt/task identity, body/destination digest, and registry/config pins, then returns that row’s exact existing body/destination. It is read-only, not a generic lookup, and adds no store/schema.

`_send_nutrition_coaching_tick` handles an existing **prepared weekly** receipt before report rendering: read its pinned material, revalidate live authority/current destination against pins, transition to `sending`, and send exactly the pinned body. Failure marks unknown and sends nothing. This path makes zero postprocessor/OpenAI calls. Existing `sending`, `delivered`, `unknown`, `abandoned`, and `sent_audited` reconciliation/no-retry behavior remains unchanged.

### Versioned adaptive adapters

`AdaptiveOperatorService.presentation_identity(callback_data, payload)` validates a fresh card’s live session/action, proposal digest, revision, envelope/provenance, and authority. It supplies internal identity material only; it exposes no identity to user/model/telemetry and does not alter `NutritionProposal`, canonical body digest, buttons, terminal state, or recovery.

New gateway `AdaptiveSurfaceAdapter` consumes the full enveloped `payload["text"]`, emits a `CanonicalSurface`, and only runs immediately before `mark_publish_pending`.

1. **`adaptive.dualcoachtest.card.v1`:** require exact envelope separator `\n\n`; title beginning `적응형 영양 검토`; one `현재 판단: ` line before `\n\n권장안\n\n`; optional exact `\n\n검토 필요\n\n` block ending at `\n\n상세 근거\n\n` or `\n\n고객에게는 아직 전달되지 않았습니다.`. Slot the whole judgment line as `adaptive.dual.judgment` and each review line in source order as `adaptive.dual.review.N`. Goals/facts/recommendations/note/customer fields/status/envelope/buttons/blank lines are locked.
2. **`adaptive.physique.card.v1`:** require exact envelope separator and current body grammar: customer/day/D+ first line, `결정:`, `근거:`, optional structured target/cycle/meal/note lines, and exact final `상태: 운영자 승인 전 · 고객에게 자동 전달하지 않음`. The adapter’s final canonical projection inserts immediately before status:

   ```text
   검토 안내

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

   ```

   Only that sentence is `adaptive.physique.review_instruction`; heading and all source lines are locked. It is code-owned presentation, derives no new proposal field, and changes no approval/delivery semantics.

Any mismatch (including future schema, `view`, menu, terminal, malformed envelope, unexpected order) is original canonical/no-call. Fresh known card selected-or-canonical payload is what `mark_publish_pending` pins. Pending recovery, view/menu, terminal delivery, and published-card recovery are exact pass-through with zero cosmetic calls.

## Private anchors, exact grammar, and validator

Each adapter builds a private slot manifest: slot id/grammar/character cap/exact prefix and suffix; internal semantic/fact/action ID; exact NFC local expansion; opaque slot-local anchor; expected sequence and role. Internal semantic/fact/action IDs never leave process. The provider receives only the opaque artifact key, surface, slot/grammar ID, anchor token, and role/order; it receives neither canonical body, raw artifact/session/event/schedule/proposal IDs, customer values/labels/notes, nor history.

Provider output must be valid UTF-8 JSON ≤16 KiB, duplicate-key rejecting, with exactly:

```json
{
  "schema_version": "korean-expression-v3",
  "surface": "daily|weekly|adaptive_operator",
  "artifact_key": "opaque-hmac-key",
  "slots": [{
    "slot_id": "...",
    "grammar_id": "...",
    "anchors": ["[[S:01]]"],
    "generated_ko": "model-authored Korean anchor composition"
  }]
}
```

No body/offset/tool/prompt echo/semantic ID/fact ID/action ID/`variant_id` is valid. A syntactically valid exact-canonical composition is canonical fallback, not a selected result.

### Grammar v1

Wire text must already be NFC (no normalization/repair), printable and control-free, one line with no leading/trailing whitespace. Anchor syntax is exactly `\[\[(?:S|F|A|T):[0-9]{2}\]\]`. The lexer scans anchors first; every other Korean/space/punctuation sequence must be consumed by this grammar. Raw expansion text, digits, date/unit/name text, Latin, an unlisted Hangul lexeme, negation/modality, medical/safety/action/delivery/approval vocabulary, or alternate bracket is rejected.

```text
LEAD        ::= "" | "정리하면, " | "확인 결과, " | "기록상, "
JOIN        ::= " 또한 " | " 그리고 "
STATEMENT   ::= <statement-anchor> "."
LIST        ::= LEAD STATEMENT (JOIN STATEMENT)*
DAILY       ::= LEAD <statement-anchor> "."
RATIONALE   ::= LEAD <statement-anchor> "."
JUDGMENT    ::= LEAD "현재 판단은 " <decision-anchor> "입니다."
REVIEW      ::= LEAD <statement-anchor> "."
INSTRUCTION ::= LEAD <statement-anchor> "."
```

- daily: `DAILY`, one S statement anchor (complete or incomplete canonical proposition).
- weekly interpretation: `LIST`, one/two ordered S sentence anchors from canonical interpretation; weekly rationale: `RATIONALE`, one S anchor.
- dual judgment: `JUDGMENT`, one T decision anchor; each review: `REVIEW`, one S anchor.
- physique instruction: `INSTRUCTION`, one A statement anchor for the code-owned instruction.

The LLM authors the Korean framing/connective composition rather than selecting a locally coded alternative. No unanchored proposition can enter: semantic clauses and decision terms are private exact anchors. Examples, never sent with their expansions: daily `정리하면, [[S:01]].`; weekly `확인 결과, [[S:01]]. 또한 [[S:02]].`; dual `기록상, 현재 판단은 [[T:01]]입니다.`; physique `확인 결과, [[A:01]].`.

### Validation

Reject invalid UTF-8/BOM/control/NFC/size; duplicate keys at every depth; wrong/missing/extra/reordered keys, slots, IDs, grammar, or anchor arrays; type confusion/bools; malformed/nested/cross-slot/new/missing/duplicate/reordered anchor; literal private expansion; and any grammar failure. Parse every slot atomically, substitute each issued anchor exactly once/in order, rebuild the final body, and compare every locked byte: headings, facts, numeric values, sections, blank lines, decision/action order, status, envelope, and buttons. Check final Telegram UTF-16 length. Reject the whole document on one failure—no partial acceptance, repair, best-effort extraction, or retry.

## Exact OpenAI-compatible identity, parser, and request

`KoreanExpressionConfig.from_extra` in new `gateway/platforms/korean_expression.py` is called from `TelegramAdapter.__init__` and alone parses `config.extra["korean_expression_postprocessor"]`. Absent/invalid means disabled. A present mapping has exactly:

```json
{
  "schema_version":"korean-expression-v3",
  "mode":"disabled|shadow|enabled",
  "surfaces":{"daily":true,"weekly":true,"adaptive_operator":true},
  "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 the literal tuple above. Exact key set/equality is required: real booleans; integer 3 not bool; ASCII strings with no trim/normalization. `auto`, proxy/aggregator/custom endpoint, alternate model, Responses/Codex/OAuth, Anthropic, Kimi/Moonshot, and pools are invalid configuration. Invalid/disabled/surface-off creates no client/claim/request/outcome; an existing downstream body replays exactly and absent body is canonical.

The sole credential source is nonempty whitespace-free process `OPENAI_API_KEY`, read only when constructing the direct client. Never read config keys, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, pools, OAuth/session credentials, main-model defaults, or auxiliary credentials; those cannot override the literal endpoint. Missing/invalid key records `credential_unavailable` for an otherwise authorized attempt and makes no HTTP request.

Construct exactly `openai.OpenAI(api_key=OPENAI_API_KEY, base_url="https://api.openai.com/v1", max_retries=0, timeout=3.0)`. Make exactly one non-streaming `chat.completions.create` with `model="gpt-4.1-mini"`, `n=1`, `temperature=0.7`, `max_tokens=384`, `response_format={"type":"json_object"}`, `store=False`, no tools and no `user`. Fixed static instructions plus redacted manifest only. No SDK/application retry/fallback/client chain. Four-second owner deadline; late result is discarded.

## No selected-presentation store; bounded outcome journal and claims

There are no sealed/encrypted artifacts, ciphertexts, content digests, prompts/responses, or selected-slot replay storage. A selected body survives only through existing weekly `body` or adaptive `card_payload`; daily adds no persistence. New private `data/korean-expression-v1/` holds only outcome metadata: root 0700; random 32-byte 0600 `identity.key`, 0600 lock/outcomes journal; no-follow creation/UID/mode validation/fsync.

`artifact_key = HMAC-SHA256(identity.key, b"korean-expression-artifact-v1\\0" || length-prefixed UTF-8-NFC authoritative fields)`. Fields are daily `(surface, finalized_event_id)`, weekly `(surface, CustomerScheduleTask identity)`, adaptive `(surface, session, proposal digest, revision, card-kind)`. Raw fields never persist/log/transmit. HMAC hides identity; it encrypts nothing.

Each canonical JSONL record has only `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`; canonical NFC sorted JSON + LF, SHA-256 chained digest. No body/prompt/response/anchor/customer/address/value/provider/model/exception/content-derived digest is recorded. Bad key/path/symlink/permissions/torn chain/sequence makes output canonical with zero calls.

Allowed outcomes are only: started `attempt_started`; final `selected`, `shadow_selected`, `credential_unavailable`, `provider_error`, `timeout`, `invalid_response`, `validation_rejected`, `commit_failed`, `publication_failed`, `claim_expired_canonical`. There is no `provider_disallowed`: a disallowed identity is invalid config before attempt.

1. Eligible owner lock-appends/fsyncs `attempt_started` with random attempt id and four-second deadline before the only wire request.
2. A concurrent unexpired observer makes no call and does **not** terminalize a healthy owner; returns `in_flight` with `publication_permitted=false`, so it cannot edit/reserve/send competing canonical bytes.
3. Owner accepts output only while its own row is still started before deadline. Candidate exists only in process memory. It must first write selected body via existing weekly/adaptive durable writer, then finalizes `selected`; daily finalizes only after initial Telegram edit succeeds. Writer/edit failure is `commit_failed`/`publication_failed`, never an unpinned selected publish.
4. Crash before durable write leaves only started metadata. Restart before expiry is in-flight/no publication; after expiry appends `claim_expired_canonical`, makes no model call, and can continue canonical. Crash after weekly/adaptive durable write but before terminal metadata recovers that existing durable body exactly.
5. Owner timeout terminalizes `timeout`; a late worker verifies ownership/state/deadline, discards result, and cannot write selected output. Shadow can terminalize `shadow_selected` but always uses canonical.

Therefore one artifact has at most one authorized direct attempt. Replay/recovery without an already durable final body is canonical-only with zero calls. Telemetry is injectable/nonblocking and exactly `{event,contract_version,surface,mode,outcome,latency_bucket}`; it never includes content, identity, anchors, values, provider/model, exception, or length.

## File-level execution sequence

1. Add Hermes `gateway/platforms/korean_expression.py`: config/identity, adapters, manifest/grammar/strict validator, direct client, outcome journal, coordinator. No profile imports or auxiliary router.
2. Modify Hermes `physique_checkin_config.py`, `physique_checkin.py`, `telegram.py`: reject legacy flag; atomic final artifact access; daily cutover; weekly-before-reserve and prepared-reader branch; fresh adaptive postprocess before `mark_publish_pending` only.
3. Modify Hermes `nutrition_coaching.py` only for fresh-card `presentation_identity` validation.
4. Modify both profile `customer_schedule.py` and `__init__.py` only for pinned prepared-material read API/dataclass.
5. Update only `듀얼코치_사용설명서.md`; leave pinned `PILOT_RUNBOOK.md` unchanged.

## Tests, controlled real-model proof, rollout

Add `/home/cube/projects/richard/hermes-agent/tests/gateway/test_korean_expression.py`:

- canonical daily/weekly/adaptive goldens; selected reconstruction and every locked byte invariant;
- dualcoachtest-v1 and physique-coach-v1 golden/selected/fallback tests, physique gateway-only projection, unknown-shape/no-call;
- response/grammar/parser attacks (duplicate JSON keys, UTF-8/BOM/control/NFC, type/field/order/anchor errors, raw expansion/value/name, negation/modality/action/safety/delivery/approval words, `variant_id`, canonical selector, layout change);
- metadata security/chain failures and fixed opaque-identity vectors; no content persistence/call;
- in-flight winner/loser, crash-before-response, restart-before/after lease, timeout-late response, crash-before downstream write, crash-after durable write, replay; exactly one call and no duplicate publication;
- fake direct HTTP boundary: one call for network/429/5xx/timeout, `max_retries=0`, literal endpoint/model/request, credential-only policy, invalid identity/no client, no auxiliary resolver/content logging.

Update Hermes `test_telegram_physique_checkin.py` for daily cutover/legacy rejection/atomic identity/selected-canonical-disabled-replay; `test_adaptive_nutrition.py` and `test_telegram_group_gating.py` for fresh-card persistence/race/recovery zero call; weekly prepared crash test for exact stored body, zero LLM calls, one Telegram authority. Add source-renderer shape goldens in **both** profile `tests/test_adaptive_nutrition.py`, plus the narrow prepared-material API tests in both profiles. Do not modify auxiliary/Moonshot production tests except an optional direct non-call assertion. Add non-default `live_korean_expression` pytest marker in Hermes `pyproject.toml`.

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_adaptive_nutrition.py tests/gateway/test_telegram_group_gating.py tests/gateway/test_nutrition_coaching.py
pytest -q /home/cube/.hermes/profiles/physique-coach/workspace/checkin_cli/tests/test_adaptive_nutrition.py
pytest -q /home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli/tests/test_adaptive_nutrition.py
```

Add `tests/gateway/test_korean_expression_live_shadow.py`, marked/skipped unless `RUN_KOREAN_EXPRESSION_LIVE=1` and `OPENAI_API_KEY` exist. It calls coordinator directly with three synthetic daily/weekly/adaptive manifests; temporary profile root; no Telegram token/client; all Telegram transport constructors/sends monkeypatched to raise. It permits only direct OpenAI, requires `shadow_selected` per surface, never calls `reserve_customer_task_delivery`/`mark_publish_pending`, captures stdout/stderr/logging/telemetry, asserts no model/manifest content leaks, and reports only surface pass/fail. Intentional command:

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

Promotion: absent/disabled default; normal suite plus all-three-surface controlled proof before shadow; remain shadow until each surface has a `shadow_selected` outcome and no validation/provider/timeout outcome in release observation; enable one surface at a time. Rollback is disabled: no new calls, existing selected weekly/adaptive bytes stay exact, and absent body is canonical.

## Attribution

The guide adds a non-executable attribution paragraph with immutable sources:

- `https://github.com/Gaeduck-0908/im-not-ai-kiro/tree/901e378ae7f77035d17a491067343ac3f83d0214`
- `https://github.com/epoko77-ai/im-not-ai/tree/53e24e8f92cf344efcb812103f7c2b203e7efffc`

It says the MIT-licensed projects were consulted only for the high-level preservation goal; this feature independently defines adapters, grammar, validation, persistence, provider, and rollout. It explicitly says no upstream source code, prompts, rules, workflow, or artifacts are copied, bundled, or claimed verbatim. No MIT notice/copyright/`SOFTWARE` text is reproduced; guide tests assert the links and exact non-copy statement only.

## Acceptance gates

1. Disabled is byte-identical canonical.
2. Real LLM produces a validated noncanonical Korean composition—not a finite local variant—on daily, weekly, dualcoachtest adaptive, and physique-coach adaptive.
3. Both adaptive schemas have versioned golden/selected/fallback coverage; unknown shapes are canonical/no-call.
4. All semantics/layout/values/status/buttons remain code-owned and locked.
5. One direct OpenAI request maximum per identity; zero retry/fallback/Kimi/auxiliary call; zero provider calls for disabled/invalid/replay/recovery/in-flight loser/unknown shape.
6. No encrypted or feature-owned selected presentation store; existing weekly/adaptive body only replays; every other recovery is canonical/zero-call.
7. Prepared weekly recovery sends exact pinned body through existing authority.
8. Controlled real-model shadow proof never sends Telegram and never logs content.
