## Summary

검토 대상은 `/home/cube/projects/richard/traning coach/.gjc/_session-019f8455-334a-7000-99ca-318dfd0e06b1/plans/ralplan/019f8455-334a-7000-99ca-318dfd0e06b1/stage-02-revision.md` (provided sha256 `32e0f71821b5aa38913b0383f3f1e5adb8768aa05d48894484555c4981e1f971`, stage_n `2`)입니다. 이전 BLOCK의 trainer 권한, activation evidence, Telegram 장애시 owner 명령, privacy lifecycle, KPI ledger는 코드 경계를 대조해 구현 가능한 수준으로 보강됐습니다. 그러나 failed retry의 immutable attempt 계약, loopback dashboard의 검증 가능한 owner identity, profile/gateway schema-skew handshake가 아직 미정이므로 BLOCK / REQUEST CHANGES입니다.

## Claims

- 현재 registry는 customer와 owner만 갖고 enabled를 직접 변경한다: `customer_coaching.py:102-145`, `customer_admin.py:104-122`. Revision은 trainer 주소/role, 전역 tuple·space disjointness, revoke/correction, typed session과 fail-closed tests를 Phase 1에 지정한다 (revision:31, 87-98).
- 현재 activation은 evidence 없이 mutable `enabled`를 toggle한다 (`customer_admin.py:104-122`). Revision은 append-only consent/provider/address/plan/token/test-delivery receipt를 요구하고 stale/mismatched evidence에서 fail-closed 한다 (revision:32, 62-69).
- 현재 owner Telegram 흐름은 draft를 owner message로 렌더할 뿐 final delivery/audit을 하지 않는다 (`telegram.py:4053-4089`). Revision은 owner Telegram approve/send command와 web의 shared `ApprovalDeliveryService`를 새 work로 지정한다 (revision:12, 39-46, 108-124).
- 현재 grounding은 customer profile 전체와 finalized check-in을 model payload로 직렬화한다 (`customer_grounding.py:39-67`). Revision은 purpose/field allowlist, durable revoke/delete cutoff, deletion/backup/restore test를 지정한다 (revision:115-124).
- 현재 schedule은 every day/monthly task를 생성한다 (`customer_schedule.py:31-49`) and current reporting cannot determine payments, satisfaction, or work time. Revision defines weekday generation, waivers, and a customer-scoped pilot ledger with inconclusive semantics (revision:91-98, 160-170).
- Current loopback dashboard auth is a process-lifetime session token injected into its SPA; OAuth `request.state.session` is only populated where `auth_required` is true, which loopback startup disables (`web_server.py:442-459`, `web_server.py:11633-11688`, `dashboard_auth/middleware.py:179-250`, `web_server.py:12763-12845`). Existing dashboard APIs also allow client-selected profile scoping (`tests/hermes_cli/test_web_server_profile_unification.py:1-80`).
- Existing profile/gateway compatibility has no negotiated capability: profile schemas use static `RegistryDocument.version: Literal[1]` and `Event.schema_version="1.0"` (`customer_coaching.py:115-145`, `models.py:102-123`), while gateway config has only `registry_path` and enabled setting (`nutrition_coaching_config.py:13-29`) and imports the profile workspace dynamically (`telegram.py:3974-3997`).

## Analysis

### Stage 1 — Spec compliance

The revision preserves the fixed pilot shape: one paying customer, trainer input over Telegram, human approval, Richard-only internal web, no AI direct delivery, weekday first-four-week operation, and conjunctive evidence-based KPI. The revised actor model, activation gate, privacy cutoff, and ledger directly close five of the seven original findings.

The outbound contract correctly abandons unprovable external exactly-once and requires a durable pre-send claim plus no automatic resend after an ambiguous outcome. But it assigns a single key to an approved revision while allowing retry after `failed`, leaving retry identity and audit behavior undefined. That keeps the central safety contract unimplementable without inference.

### Stage 2 — Architecture

**Trainer/activation/privacy/ledger:** The new customer-scoped modules and Phase 1/2/4 acceptance cases are appropriately placed beside the existing registry, event store, schedule, grounding, and reporting boundaries. The existing code confirms these responsibilities are absent today, so adding rather than pretending to reuse them is the correct direction.

**Delivery:** Separate immutable approval revisions from immutable delivery attempts. A revision is approval history; an attempt is an externally observable call. An active-claim lookup may be unique per revision, but a retry needs a new explicit attempt that references the old definitive failure. Ambiguous attempts must never be silently re-opened or transformed into a retry.

**Web boundary:** The selected built-in route is a viable MVP architecture and its Host/Origin/CSRF/cache/referrer/PII requirements are substantially stronger than the prior plan. The antithesis is that localhost + SSH tunnel only protects network reachability; it does not provide an application-level owner identity. In the cited loopback implementation, the bearer token authenticates possession only and is injected into the UI; its OAuth Session identity is unavailable. The plan must state exactly what establishes owner identity and console-local 30-minute idle expiry before a health-data route relies on it.

**Deployment skew:** Flags-off deployment and reader-aware rollback are good operating constraints, but they are not a handshake. The two source roots presently share no version negotiation. An explicit profile capability manifest and gateway/dashboard compatibility check need an owner and tests, otherwise a deployer must infer what version combinations are safe.

### Constructive synthesis

1. Add `DeliveryAttempt` events keyed by `attempt_id`, with `approved_revision_id`, `retry_of`, state, provider receipt, reconciliation observation, and one active-claim record per revision. Specify transitions for definitive failure, ambiguous resend authorization, revoke/delete between claim and send, and all channel races.
2. Select one owner-auth source for loopback—prefer an explicitly configured local owner credential/session bound to the active `physique-coach` profile, because the current OAuth `Session.user_id` does not exist in the chosen mode. Define issuance, idle expiry, rotation/revocation, CSRF binding, and no-identity denial. Alternatively enable a specific OAuth mode over the tunnel and allowlist its `Session.user_id`; do not label the existing injected bearer as a principal.
3. Introduce a profile capability/version manifest consumed by gateway and dashboard route registration. It must declare readable/writable event/registry/decision/delivery schemas, reject mismatches before any activation/schedule/delivery action, and be exercised with old-reader/new-writer, old-gateway/new-profile, feature-off, and rollback fixtures.

## Root Cause

The revision resolves most domain omissions, but still names three cross-process safety properties without providing their durable identities: a retry attempt, an authenticated loopback owner, and a profile/gateway compatibility capability. Existing code has neither identity on its loopback bearer path nor a schema handshake, so those properties cannot be inherited from the selected dashboard or gateway.

## Findings

1. **HIGH — revision:35-45 — Delivery retry has no immutable attempt identity.** One revision key and the terminal-looking delivery state cannot both preserve append-only audit history and allow retry after `failed`. Create immutable attempts with `retry_of` and a separate active-claim invariant; retain ambiguous attempts as non-retriable absent explicit resend authorization. Reported as P1.
2. **HIGH — revision:11-12, 142-149 — Loopback owner principal is not specified by an existing session mechanism.** Loopback uses an injected process bearer, while authenticated `Session` identity only exists in non-loopback OAuth mode; dashboard profile selection is user-controlled. Specify the principal source, active-profile binding, 30-minute idle state, expiry/revocation, and deny tests for the actual loopback mode. Reported as P1.
3. **HIGH — revision:172-192, 210 — Schema compatibility handshake has no owned contract or gate.** Current profile and gateway code have independent static schema markers and no negotiated capability. Assign a manifest/checker to concrete modules and block console registration, activation, scheduling, and send on mismatch; test mixed-version deployment and rollback. Reported as P1.

## Recommendations

1. Resolve all three HIGH findings before implementation and move their contracts into the canonical domain/storage and Phase 3/5 sections.
2. Retain the selected built-in dashboard route, shared delivery service, append-only evidence records, pseudonymous provider allowlist, and measured pilot ledger; these are the correct replacements for the current mutable registry/schedule/draft-only seams.
3. Add focused tests that model retry sequence/attempt graph, loopback unauthorized/expired/other-profile access, and schema-mismatch startup/feature/rollback behavior in addition to the listed E2E rehearsal.

## Architectural Status

BLOCK

## Code Review Recommendation

REQUEST CHANGES

## Tradeoffs

| Choice | Benefit | Required constraint |
|---|---|---|
| Loopback built-in route with dedicated local owner credential | Reuses dashboard lifecycle while matching SSH-only operation | Explicit credential, profile binding, idle expiry, CSRF and rotation; current injected bearer is insufficient as a named principal |
| Loopback dashboard with OAuth owner allowlist | Reuses `Session.user_id` authorization | Must make OAuth auth active for the selected deployment and manage tunnel/cookie behavior; more operational setup |
| Immutable delivery attempts | Preserves audit and supports definitive-failure retry | Enforce a single active claim per revision and no automatic ambiguous retry |
| Revision-level mutable delivery state | Fewer records | Cannot simultaneously model append-only history, retries, and channel-race safety; reject |
