# Revised RALPLAN — guarded Korean expression selection v2

## Objective and approval boundary

Replace the unsafe free-form daily feedback insertion with a finite, code-owned Korean acknowledgement selection. The provider receives no check-in text, identifier, fact, hash, or rendered body, and can return only a local `variant_id`. Kimi/Moonshot is prohibited: the selector has no provider, credential, model, or model-family fallback.

V1 is intentionally narrower than the original plan. It has one daily cosmetic slot only; current weekly reports and Topic 59 adaptive-review cards have **zero** eligible slots because their current bytes contain no safely nonsemantic span. This is planning only. Active-turn and conversation model features remain out of scope and keep their existing 30-second behavior. No customer activation, new delivery permission, Gate-D claim, or direct data-store edit is approved.

## Inspected evidence and decisions

- `gateway/platforms/telegram.py:_nutrition_daily_text` passes `feedback` into `_nutrition_daily_interpretation`, which permits two arbitrary model lines after only five token checks. `_saved_physique_coaching_feedback` calls the model after Save and `_render_physique_feedback_replay` calls it again. These paths must be removed, not hidden behind a stronger prompt.
- `_send_nutrition_coaching_tick` calculates the weekly body before `template_digest` and `reserve_customer_task_delivery`. Any post-reservation body mutation violates the existing immutable delivery pins. `_nutrition_report_text` has no static courtesy sentence that can vary while retaining the disabled fixture; weekly is zero-slot in v1.
- `render_operator_card`, `_handle_adaptive_review_callback`, and `_recover_pending_adaptive_cards` publish/recover durable audit and lifecycle payloads. They must remain exact pass-through.
- `agent/auxiliary_client.resolve_provider_client` builds OpenAI clients without `max_retries=0`; `pyproject.toml` pins `openai==2.24.0`, whose default retries can make a single Python call several wire attempts. The Anthropic wrapper defaults to stream-then-create fallback. The selector needs a separate, verified one-wire policy.
- `agent/moonshot_schema.is_moonshot_model` plus `agent/anthropic_adapter` already identify broad Kimi/Moonshot model and endpoint families; selection will use one public consolidated predicate instead of a pre-resolution `"kimi"` substring.
- Gateway tests import the `physique-coach` package while the original plan targeted `dualcoachtest`. The new contract is shared Hermes code, so neither profile-local `checkin_cli` package owns or duplicates it.

## Fixed rendering contract

### Canonical daily cutover

Refactor `_nutrition_daily_text` to accept only `snapshot`; delete its `feedback` argument. Replace `_nutrition_daily_interpretation(feedback)` with a deterministic helper accepting the already-computed `missing` boolean:

| Condition | Exact canonical interpretation | Eligible slot |
|---|---|---|
| `not any(fact.endswith("기록 없음") for fact in facts)` | `저장된 오늘 기록을 확인했습니다.` | `daily.acknowledgement.v1` |
| Any fact row is missing | `일부 항목이 기록되지 않아 저장된 내용만 안내합니다.` | none |

The complete canonical form is exactly:

```text
오늘 체크인 완료

{the seven existing local fact bullets}

저장된 오늘 기록을 확인했습니다.

오늘 할 일

{the existing local action bullets}
```

The incomplete form uses the second literal in the same location. Fact formatting, heading, blank lines, action predicates, bullet ordering, and all calculations remain local and unchanged. The guide's complete-data sample intentionally changes from the two legacy free-form conclusion lines to `저장된 오늘 기록을 확인했습니다.`; the incomplete sample remains unchanged. Already-sent Telegram messages are not rewritten. Legacy saved-event replay uses this canonical renderer and never retrieves, regenerates, appends, or displays historic free-form feedback.

Delete `_saved_physique_coaching_feedback`, `_generate_physique_coaching_feedback`, and `_request_physique_coaching_feedback`; remove `coaching_feedback_enabled` from `PhysiqueCheckinConfig` and its parser. Keep `_request_physique_coach_completion`, `_request_physique_active_turn`, and `_request_physique_conversation_reply` only for unrelated active-turn/conversation callers. The new daily path must not call any of those helpers.

Add `PhysiqueCheckinBridge.finalized_coaching_artifact(session_id)` and `latest_finalized_coaching_artifact()`, returning one validated private `(snapshot, finalized_event_id)` pair. Do not put the ID in the snapshot or model input. Use only `daily:<finalized_event_id>` as the opaque decision key. A corrected event gets a new ID and is a new logical artifact.

### Literal v1 slots and parser

Create `gateway/platforms/korean_expression.py`; do not implement this contract in either profile-local `checkin_cli` package. It owns frozen/slotted dataclasses, exact local strings, strict parsing/rendering, decision persistence, config parsing, and telemetry values.

| Surface | Slot | Canonical | Only alternative | Placement/predicate |
|---|---|---|---|---|
| `daily` | `daily.acknowledgement.v1` | `저장된 오늘 기록을 확인했습니다.` | `오늘도 빠짐없이 기록해 주셔서 감사합니다.` (`warm_ack_v1`) | whole interpretation line between fact block and `오늘 할 일`; only when every fact is present |
| `weekly` | none | existing `_nutrition_report_text` output | none | always pass-through |
| `adaptive_operator` | none | durable service payload | none | always pass-through |

Both strings are NFC-normalized local constants and contain no metric, date, target, diagnosis, treatment, judgment, action, follow-up, approval/delivery state, heading, or customer-specific assertion. Daily construction is `prefix + canonical_slot + suffix`, with the exact character slice recorded while rendering. Before substitution the renderer checks the canonical slice and byte-identical locked prefix/suffix, then verifies headings, blank-line boundaries, facts, and action bullets. Reject stale, overlapping, out-of-bounds, or mismatched slices. The model supplies neither text nor offsets.

The constant, unlogged request is:

```json
{"schema_version":"korean-expression-v1","surface":"daily","slots":[{"slot_id":"daily.acknowledgement.v1","allowed_variant_ids":["canonical","warm_ack_v1"]}]}
```

The only accepted response is one UTF-8 JSON object no larger than **512 bytes**:

```json
{"schema_version":"korean-expression-v1","surface":"daily","selections":[{"slot_id":"daily.acknowledgement.v1","variant_id":"canonical|warm_ack_v1"}]}
```

Require exact top-level/selection keys, non-bool strings, exact version/surface, exactly one ordered selection, exact slot ID, and local allow-listed ID. Reject fences/prefixes/suffixes, non-object JSON, duplicate keys at every level (`object_pairs_hook`), control-character IDs, missing/extra/reordered selections, and oversize content before parse. Do not normalize or repair provider text. Every failure returns the supplied canonical body.

## Durable at-most-one daily attempt

Use a shared append-only decision ledger at the validated profile root: `data/korean-expression-decisions-v1.jsonl` and `.korean-expression-decisions-v1.lock`. It is separate from adaptive EventStore and scheduled-delivery ledger; do not add a proposal field, adaptive epoch, delivery row, text hash, or content-bearing audit event.

Each hash-chained row contains only schema version, sequence, opaque logical key, surface enum, mode (`shadow|enabled`), state (`attempt_started|finalized`), finite outcome, optional local variant ID, previous-row digest, row digest, and timestamp. It never contains prompt/response/body/raw length/text digest/customer key/name/chat/topic/message ID/numeric value/exception string. Validate all rows, sequence/digest chain, file mode/ownership, and non-symlink root/data/ledger/lock before use. Use exclusive locking, append+fsync, and directory fsync. Any corrupted, partial, untrusted, or unavailable store produces canonical copy and **zero** provider calls.

State machine:

1. For valid `shadow` or `enabled`, eligible daily rendering appends `attempt_started` atomically **before** a request. Only that lock winner has attempt authority.
2. It makes one verified wire attempt and conditionally appends `finalized`, either selected local ID or a finite canonical outcome, before returning a body.
3. A concurrent renderer finding `attempt_started` never waits or makes another request. It atomically terminalizes it as `claim_incomplete_canonical` and returns canonical.
4. A late original worker conditionally finalizes, observes terminal state, discards its response, and returns the terminal canonical result. Crash after claim, after request, or before final append cannot cause a second call or apply a different result.
5. A terminal enabled selection replays its persisted local variant. Shadow selection, every error, no-row legacy replay, and incomplete claim replay canonical. Disabled/invalid config creates no new claim/call but respects a terminal decision so an already-rendered artifact cannot be recomputed or changed.

This is the precise daily crash/concurrency/replay guarantee. Weekly and adaptive are zero-slot, so concurrent weekly ticks make zero cosmetic calls and retain the existing one-delivery protocol.

## Strict configuration and telemetry

Parse only `config.extra["korean_expression_postprocessor"]` in `TelegramAdapter.__init__`. Absent mapping becomes:

```json
{"mode":"disabled","daily":false,"weekly":false,"timeout_seconds":3}
```

A supplied mapping must have exactly those four keys: mode exactly `disabled|shadow|enabled`, daily/weekly actual booleans, and timeout an actual integer (not bool) from 1 to 5. Unknown/missing keys, wrong types, or invalid range are invalid configuration and fail closed to disabled. It cannot override provider/model/endpoint/credential/destination/feature epoch/delivery permission. Do not add it to adaptive `_FEATURE_FLAG_NAMES`.

- **disabled:** new artifacts return canonical with no claim/resolution/request; a terminal decision still replays immutably.
- **shadow:** eligible daily artifacts claim and validate one request, persist shadow outcome, emit telemetry, and return canonical. Shadow can never later become apply.
- **enabled:** eligible daily artifacts follow the same claim path and can render only their persisted local ID.
- **weekly:** always `no_eligible_slot`; body/digest/delivery are unchanged under all modes.
- **adaptive:** always exact pass-through. In `_handle_adaptive_review_callback`, only for a new `publication_status == "card"` payload just before publication, emit nonblocking `adaptive_operator/no_eligible_slot`. Do not invoke the postprocessor, mutate body/buttons, persist a decision, or emit from `_recover_pending_adaptive_cards`.

Default telemetry calls `logger.info("korean_expression_postprocessor", extra={"korean_expression": event})` through an injectable sink. Event keys are exactly `event`, `contract_version`, `surface`, `mode`, `outcome`, and `latency_bucket`; no other keys. Values are bounded: event `korean_expression_postprocessor`; version `korean-expression-v1`; surface `daily|weekly|adaptive_operator`; strict mode; outcome `disabled|invalid_config|no_eligible_slot|selected|shadow_selected|replay_selected|replay_canonical|provider_disallowed|unsupported_transport|provider_error|timeout|invalid_response|validation_rejected|decision_store_unavailable|claim_incomplete_canonical`; latency `not_attempted|0_250ms|251_1000ms|1001_3000ms|3001_5250ms|over_5250ms`. Catch and ignore sink failure so it cannot alter rendering, persistence, reservation, or send behavior.

## Resolved Kimi exclusion and one-wire transport

Add one public `agent.moonshot_schema.is_kimi_moonshot_identity(provider, model, base_url)` and use it exclusively in selection. NFKC-normalize/casefold; deny provider aliases/names including `kimi`, `moonshot`, `moonshotai`, `kimi-coding`, `kimi-coding-cn`; model IDs `kimi`, `kimi-*`, `kimi_*`, `moonshot-*`, `moonshot_*`, `moonshot-v1-*`, vendor paths containing `moonshot` or `/kimi`, and tails `k1.`, `k1-`, `k2.`, `k2-`, `k25`, `k2.5`; and `api.kimi.com`, `moonshot.ai`, or `moonshot.cn` hosts/subdomains including `/coding`.

Add a restrictive policy in `agent/auxiliary_client.py`, used only by selection, that returns credential-free `ResolvedProviderIdentity(client, provider_id, model, base_url, transport)`. It does not auto-select a provider, credential pool entry, model fallback, or configured fallback; it rejects `auto`, custom/unknown/external-process routes, and transport shapes without provable no-retry behavior. It supports only resolved raw OpenAI Chat Completions, Codex Responses, and Anthropic Messages paths with their final identity exposed before completion.

The selector may reject an obvious configured Kimi label early, but must always resolve under that restrictive policy, then apply the public predicate to both configured and resolved identity before `.create`. It additionally requires a compiled safe provider/endpoint allow-list. Thus a benign alias resolving to Kimi, Kimi via aggregator slug, or unverifiable private proxy returns canonical with no completion call. Identity is not telemetry.

Thread this policy through every supported constructor:

- raw OpenAI and the raw client below Codex use `OpenAI(..., max_retries=0)`;
- native Anthropic construction uses SDK `max_retries=0`; `AnthropicAuxiliaryClient` carries the policy;
- policy-mode `_AnthropicCompletionsAdapter` uses `create_anthropic_message(..., prefer_stream=False)`, never stream-then-create;
- Codex has one `responses.create` stream and no fallback;
- unsupported wrappers return `unsupported_transport`, canonical, and zero requests.

Send only the constant document with no tools and validated timeout. Run one synchronous worker in `asyncio.to_thread` under deadline `timeout_seconds + 0.25`. On timeout close the client best-effort, terminalize canonical, and discard a late result through conditional finalize. No application retry, SDK retry, wrapper fallback, provider fallback, or later replay/schedule retry is permitted.

## File-level implementation steps

1. Add `gateway/platforms/korean_expression.py`: local table/doc/parser/renderer, strict config, safe telemetry values/sink, ledger/claim state machine, and async coordinator. It has no `checkin_cli` import.
2. Extend `agent/moonshot_schema.py` with the public canonical Kimi predicate; adapt existing relevant detection to it without a second divergent classifier.
3. Extend `agent/auxiliary_client.py` and relevant Anthropic builder/adapter with the restrictive resolved-identity single-wire policy. Existing resolver callers retain their retry defaults/timeouts.
4. Extend `gateway/platforms/physique_checkin.py` with private finalized artifact access that returns snapshot plus ID without exposing the ID to model or user text.
5. Update `gateway/platforms/telegram.py`: parse strict config; delete legacy daily feedback route/config; build canonical daily first; pass only daily constant document and opaque key to the coordinator; use return value for save/replay. Do not touch weekly body before digest/reservation, scheduler delivery behavior, adaptive payload/recovery bytes, or adaptive lifecycle data.
6. Do not modify either profile `adaptive_nutrition.py`; the shared module removes the prior profile-package ambiguity.
7. Update `듀얼코치_사용설명서.md` non-executably with exact canonical sample, safety/privacy/identity/one-attempt/mode/replay semantics, v1 weekly/adaptive exclusions, telemetry schema, isolated rollout/rollback, and all existing Gate-D/human-only limits.

## Verification

### New shared tests: `tests/gateway/test_korean_expression.py`

- Assert exact complete/missing canonical bytes; two local strings/NFC; slice locking; facts/actions/headings/blank lines unchanged; weekly/adaptive zero-slot pass-through.
- Parameterize fenced/non-JSON/empty/oversize input, duplicate keys, unknown/missing/extra keys, bool/nonstring fields, wrong version/surface, reordered/missing/duplicate selection, control characters, unknown ID, and stale/overlap/out-of-bounds/mismatched slots. Each returns exact canonical and retains no provider text.
- Simulate two concurrent daily coordinators, restart after claim, crash after request-before-finalize, late winner after canonical terminalization, selected replay, shadow replay, and mode-disabled replay of prior selected result. Assert at most one completion attempt and deterministic bytes.
- Verify corrupted/torn/digest-invalid/symlink/permission-failed ledger produces canonical plus zero calls. Capture normal/failing telemetry sinks; assert only six allow-listed fields and no content/id/digest/number/exception leakage.

### Provider/identity tests

- Extend `tests/agent/test_auxiliary_client.py` and add/extend `tests/agent/test_moonshot_schema.py`: verify every supported OpenAI/Codex/Anthropic constructor uses zero retries; Anthropic uses direct create, never stream fallback. At HTTP boundary fake retryable 429/5xx/connection failure/timeout and prove exactly one request, no fallback, bounded time, canonical result.
- Cover every Kimi family above, safe-looking alias to `api.kimi.com`, `moonshot-v1-8k`, `k1.5-*`, `k2-*`, vendor-prefixed aggregator slug, all listed hosts, and near-misses `kimiko`/`k3-*` constrained by safe allow-list. Assert no completion call.
- Simulate outer deadline and late success: assert close attempted, one wire attempt, `claim_incomplete_canonical` terminal, and late variant never stored/applied.

### Gateway integration: extend `tests/gateway/test_telegram_physique_checkin.py`

- Replace complete-data fixture expecting generated prose with the exact new canonical line; preserve missing fixture. Disabled output is byte-equal. Save/replay must call neither deleted feedback helpers nor `_request_physique_coach_completion`.
- Valid apply changes only the acknowledgement line. Shadow/timeout/client failure/malformed JSON/validation failure/Kimi/invalid config return canonical. Duplicate callback plus replay of same final event makes one selection call and an accepted Save still edits its confirmation when selection fails.
- Keep unrelated active-turn/conversation tests proving their existing model helper contract remains separate.
- Under every mode assert weekly text/template digest stays byte-identical and makes zero cosmetic calls; concurrent weekly ticks retain existing single Telegram delivery. Preserve unknown/reconciliation behavior.
- Adaptive card publication/recovery with cosmetic provider mock failing on call: exact payload/text/buttons/digests and zero invocation. Verify only new-card classification.
- Run subprocess-isolated fixture with Hermes source plus production `physique-coach` profile on `PYTHONPATH`; assert the contract originates at `gateway/platforms/korean_expression.py`, not either `checkin_cli` package.

Run new focused tests, focused gateway tests, existing dualcoachtest and physique-coach `test_adaptive_nutrition.py` suites unchanged, and scheduled-delivery/P2–P6 regressions. Release gate: zero legacy daily completion calls, zero Kimi calls, proven one wire per supported transport, canonical disabled/error/unresolved-replay bytes, stable selected replay, zero weekly/adaptive cosmetic calls, no duplicate delivery, and no content telemetry.

## Rollout, rollback, and attribution

1. Ship default disabled and run baseline/privacy/transport tests only in the isolated profile.
2. Use daily-only shadow (`weekly: false`) there; selection may validate once but visible bytes remain canonical.
3. Enable daily-only apply only after crash/concurrency, identity, one-wire, and telemetry gates pass. Weekly remains documented zero-slot v1.
4. Roll back with disabled: new artifacts are canonical/no-call; terminal old decisions remain solely to prevent changing a rendered artifact and contain local enum IDs only.

No rollout step enables real customers, adaptive delivery, or manual Gate-D.

Add a non-executable NOTICE in `듀얼코치_사용설명서.md` for selected principles from Kiro port `https://github.com/Gaeduck-0908/im-not-ai-kiro` and original `https://github.com/epoko77-ai/im-not-ai`: preserve meaning/facts/numbers/dates/proper nouns/quotes, make finding-local changes, retain tone/genre, avoid over-polishing, and roll back doubtful semantic edits. Do not adopt its filesystem artifact persistence, multi-agent workflow, retries, or raw input/output logging. Include verbatim:

```text
MIT License

Copyright (c) 2026 Taehan Kim
Copyright (c) 2026 epoko77-ai (Original work - https://github.com/epoko77-ai/im-not-ai)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

## Pre-mortem

| Failure | Signal | Prevention/containment |
|---|---|---|
| Prose/retry/fallback reaches provider | one-wire test or invalid response | enum-only document; strict 512-byte parser; zero SDK retries; no Anthropic stream fallback; canonical/no retry |
| Crash/race gives second or different body | claim recovery test | durable pre-call claim and conditional terminalization; canonicalize/discard late result |
| Kimi through alias/model/endpoint | identity matrix | compiled safe identity policy and post-resolution predicate; no completion call |
| Weekly/adaptive semantic mutation | digest/card bytes differ | explicit zero slots; no postprocessor at durable boundaries |
| Rollout/telemetry violation | strict-config/log allow-list failure | strict modes, nonblocking sink, disabled rollback |

## Result

This revision resolves every prior blocker: exact daily canonical cutover with no legacy prose; durable one-attempt daily semantics across crash, concurrency, and replay; resolved Kimi-family exclusion before completion; true disabled/shadow/enabled behavior; verified wire-attempt control; explicit weekly/adaptive exclusions; privacy-safe telemetry; package isolation; and preserved safety, delivery, attribution, and test requirements.
