# RALPLAN-DR Revision 4: Final Consensus Candidate

This revision incorporates all of Revision 3 and supersedes any conflicting wording with the exact contracts below.

## Atomic approval/command transaction
- Canonical log: `data/customers/<customer_key>/commands.jsonl`, mode 0600, protected by `commands.lock` using cross-process `flock`.
- A single immutable `ApprovalCommand` contains `command_id`, `approved_revision_id`, final rendered text digest, approver capability generation, approved_at, and initial `attempt_id`. Approval and first attempt are therefore one record and one fsync boundary—not two appends.
- Under the lock, the domain service scans the current projection and rejects any existing command for the same `approved_revision_id`. This is the linearization point shared by web and owner Telegram commands.
- Commit sequence: acquire lock → write complete record to a per-customer temp file containing prior bytes plus new line → flush+fsync file → atomic rename over `commands.jsonl` → fsync customer directory → release lock.
- Queue is a derived projection of committed commands; no independent queue file can strand. Gateway restart rebuilds pending work from `commands.jsonl` and immutable attempt events.
- Gateway alone creates the worker claim `claims/<attempt_id>.claim` by O_EXCL after the committed command exists. A crash before claim leaves pending; after claim but before durable `call_started` remains pending only if claim proves no call began; once `call_started` is fsynced, stale work becomes ambiguous.
- Explicit retry/resend authorization acquires the same lock and appends one new immutable attempt (`retry_of` or `resend_authorization_id`). Per revision, the projection permits at most one nonterminal attempt. Concurrent commands are tested with real processes.

## Single-message customer delivery
- Approved final feedback MUST render to one Telegram message of at most 4096 UTF-16 code units after escaping/formatting.
- The editor displays remaining rendered length and refuses approval over the limit. The domain service revalidates server-side.
- Customer delivery does not use the generic chunk-splitting helper. One attempt maps to exactly one Telegram send and one returned message_id.
- Long evidence stays in the internal console; customer final text remains concise. Boundary tests cover 4095/4096/4097 UTF-16 units, escaping expansion, emoji/surrogate pairs, timeout and crash.

## Persisted capability manifest
- Path: profile root `data/schema-capabilities.json`, mode 0600.
- Schema: `{manifest_version, generation, min_reader_version, writer_version, readable_event_versions, writable_event_versions, enabled_capabilities, updated_at, content_sha256}`.
- Owner: `checkin_cli/capabilities.py`; gateway/dashboard are read-only consumers.
- Upgrade sequence under `data/schema-capabilities.lock`: write new manifest temp → fsync → rename → directory fsync → only then permit incompatible event writes. Missing/corrupt/unknown manifests fail closed.
- Event append records the manifest generation and required reader version. Readers compare all stored-event requirements to their code capability before activation, routes, schedule or outbound execution.
- Crash tests cover before/after manifest rename, before first new event, manifest advanced without events, unknown event after rollback. Downgrade is blocked when stored requirements exceed reader version.

## Console bootstrap/replay API
- Existing dashboard launch bearer means “holder of this loopback dashboard launch capability,” not personal OAuth identity.
- `POST /api/physique-coach/session/bootstrap` requires existing `X-Hermes-Session-Token`, exact Host loopback check, configured profile `physique-coach`, feature flag and same-origin Origin.
- On success it rotates a server-memory `console_generation`, creates a 256-bit signing key owned by the dashboard process, and returns an HttpOnly `__Host-physique-coach` cookie plus CSRF token in JSON. Cookie payload: random session id, generation, profile, issued_at, last_seen, expiry; signed, not encrypted, contains no PII.
- Idle expiry is 30 minutes; each successful console request rotates cookie and CSRF token. The bootstrap bearer cannot call console mutation endpoints directly.
- Dashboard restart destroys key/session state and rotates generation. Old cookie/CSRF replay returns 401; expired returns 401; wrong profile/Host/Origin returns 403. Recovery is dashboard restart then SSH tunnel and fresh bootstrap.

## Exact profile adapter and dashboard lifecycle
- Adapter: new `hermes_cli/physique_coach_profile.py` resolves profile root only from `HERMES_HOME=/home/cube/.hermes/profiles/physique-coach`, verifies `HERMES_PROFILE=physique-coach`, and imports package root `/home/cube/.hermes/profiles/physique-coach/workspace/checkin_cli` after capability validation. Any mismatch blocks route registration.
- Use isolated profile dashboard, not unified machine dashboard.
- Unit: `/home/cube/.config/systemd/user/physique-coach-dashboard.service`, mode 0600:
  - `WorkingDirectory=/home/cube/projects/richard/hermes-agent`
  - `ExecStart=/home/cube/projects/richard/hermes-agent/venv/bin/python -m hermes_cli.main dashboard --isolated --host 127.0.0.1 --port 9120 --no-open --skip-build`
  - `Environment=HOME=/home/cube HERMES_HOME=/home/cube/.hermes/profiles/physique-coach HERMES_PROFILE=physique-coach`
  - `Environment=PATH=/home/cube/projects/richard/hermes-agent/venv/bin:/home/cube/.local/bin:/usr/local/bin:/usr/bin:/bin`
  - `UMask=0077`, `Restart=on-failure`, `RestartSec=5`, `NoNewPrivileges=true`, `PrivateTmp=true`
- Unit creation is an explicit reviewed file operation in execution, followed by daemon-reload/enable/start. Rollback disables/stops/removes only this unit and reloads systemd. Existing `hermes dashboard --stop` is run before restart/update to prevent stale processes.
- Frontend build artifact is `/home/cube/projects/richard/hermes-agent/web/dist`, built before service restart.

## Exact configuration ownership
- `hermes_cli/config.py`: `dashboard.physique_coach_approval_console=false`, `dashboard.physique_coach_profile_root`, local port 9120.
- `nutrition_coaching_config.py`: worker enabled/poll interval/capability minimums only.
- Registry hash reload occurs before every inbound route and each <=2s worker/schedule poll; independent revoke/delete tombstone always wins.

## Exact verification ledger
```bash
# Profile focused
/home/cube/projects/richard/hermes-agent/venv/bin/python -m pytest \
  tests/test_customer_admin.py tests/test_customer_coaching_domain.py \
  tests/test_customer_nutrition_wizard.py tests/test_customer_schedule.py \
  tests/test_customer_reporting.py tests/test_customer_privacy.py \
  tests/test_customer_decisions.py tests/test_customer_delivery.py \
  tests/test_customer_pilot.py
# cwd: /home/cube/.hermes/profiles/physique-coach/workspace/checkin_cli

# Profile full
/home/cube/projects/richard/hermes-agent/venv/bin/python -m pytest tests

# Hermes focused
/home/cube/projects/richard/hermes-agent/venv/bin/python -m pytest \
  tests/gateway/test_telegram_physique_checkin.py \
  tests/gateway/test_nutrition_coaching.py \
  tests/gateway/test_customer_outbound_worker.py \
  tests/hermes_cli/test_physique_coach_routes.py
# cwd: /home/cube/projects/richard/hermes-agent

# Hermes full
/home/cube/projects/richard/hermes-agent/venv/bin/python -m pytest tests

# Frontend
/home/cube/.local/bin/npm --workspace web test -- PhysiqueCoach
/home/cube/.local/bin/npm --workspace web run typecheck
/home/cube/.local/bin/npm --workspace web run build
# cwd: /home/cube/projects/richard/hermes-agent
```
All commands must exit 0. New files named above are created by the matching phase before execution.

## Health and rollout probes
- `systemctl --user is-active physique-coach-gateway.service physique-coach-dashboard.service` must print active for both.
- `curl` is not used as an implicit trust bypass; the focused dashboard route test exercises bootstrap. Operational health endpoint `GET /api/physique-coach/health` with launch bearer returns only `{profile, capability_generation, queue:{pending,sending,ambiguous}, worker_last_seen, registry_generation}` and no PII.
- Pass: profile/capability match, registry loaded, worker heartbeat <10s, no stale sending. Any mismatch blocks customer activation.
- E2E command is a new test `python -m pytest tests/e2e/test_physique_coach_pilot.py -m e2e` using dedicated test Telegram credentials; it is never run against the real customer before test-account approval.
- Fault suite: `python -m pytest tests/gateway/test_customer_outbound_worker.py -k 'crash or timeout or concurrent'`.
- Backup/restore rehearsal: `python -m pytest tests/test_customer_privacy.py -k 'backup or restore or tombstone'`.
- Rehearsal receipt stored privately as `pilot-rehearsal-<timestamp>.json` with source hashes, code versions, manifest generation, test commands/results, service health, token epoch, E2E message ids, rollback result; no customer content.

## Final Phase Order
0. Preserve/hash evidence; finalize traceability and provider/retention policy.
1. Add capability manifest, activation receipts and identity/role event contracts.
2. Add adaptive check-in, trainer records, weekday schedule and cache/tombstone reload.
3. Add privacy lifecycle, decision command transaction, immutable attempts and gateway worker.
4. Add dashboard profile adapter, bootstrap session, approval console and health projection.
5. Add pilot ledger, weekly report and four-way success predicate.
6. Run all verification, install services with flags off, test-account rehearsal, token rotation, then gated activation.

## ADR
### Decision
Use one atomic approval-command log, a derived queue, gateway-only one-message Telegram execution, persisted capability manifest, and an isolated loopback dashboard with launch-capability bootstrap.
### Drivers
No silent approved-message loss, no duplicate/partial send ambiguity beyond explicit states, crash-safe versioning, Richard-only internal operation.
### Alternatives
Separate approval/queue files, dashboard direct sending, chunked customer delivery, OAuth owner identity, unified machine dashboard and standalone sidecar were rejected for atomicity, token ownership, partial-send, unnecessary identity setup, profile ambiguity or lifecycle duplication.
### Consequences
Final customer messages are length-bounded and the system adds a dedicated dashboard unit and command log. Failures become visible pending/ambiguous states rather than automatic retries.
### Follow-ups
After the first passing four-week pilot, reassess longer customer messages, plugin extraction and formal external authentication only with measured need.

## Intent Reconciliation
No user decision changed. One external customer, 150,000 KRW monthly transfer, 12-week plan/first-four-week daily-weekly operation, customer and trainer Telegram input, Richard-only internal approval web, human approval, safety gates and conjunctive KPIs remain intact.
