# Diagnostic Isolation — Final Safety Closure Plan

## Status
Planning only. This plan incorporates Stage 28 by reference and changes only the three final blockers below.

## RALPLAN-DR

### Principles
1. Read-only audit must be physically non-mutating, not merely logically read-only.
2. Closing diagnostics must linearize before any later reservation/provider admission.
3. Security policy resources must survive packaging and installation exactly.
4. Ordinary production and all Stage-28 boundaries remain unchanged.

### Drivers
- prevent audit lock files/modes from changing source state;
- guarantee provider zero after close/expiry/stop fence;
- prevent installed deployments from silently losing the promotion policy.

### Options
- **Shared-inode read APIs + detaching generation fence + package-data proof (chosen):** preserves writer synchronization and fail-closed semantics.
- Copy ledgers without locks: rejected because snapshots can tear.
- Keep policy only in source tree: rejected because wheels may omit it.
- Drain without detaching state: rejected because new sends can enter while draining.

## 1. Physically read-only shared locks

### APIs and files
- `WizardStorage.read_locked()` in `wizard_storage.py`
- `AdaptiveEventStore.read_locked()` in `adaptive_nutrition.py`
- `schedule_delivery_read_lock(profile_root, customer_key)` plus a read-only schedule snapshot API in `customer_schedule.py`
- corresponding tests in `test_diagnostic_isolation.py`, `test_customer_schedule.py`, and existing adaptive/wizard tests.

Each reader derives the **same existing lock path/inode** used by its writer. It opens with `os.open(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC)`, verifies regular file, owner, mode 0600, link count 1 and unchanged `(st_dev, st_ino)` after open, then takes `flock(LOCK_SH)`. It never mkdirs, touches, chmods, creates, truncates, recovers or writes. Missing/unsafe/replaced locks fail closed as `snapshot_unstable` or `corrupt_state` and produce no artifact.

Writers keep `LOCK_EX` on the same inode but are hardened to open safely (`O_RDWR|O_NOFOLLOW|O_CLOEXEC`) after their existing controlled initialization. Initialization remains write-path only. Snapshot export may call only read APIs and validated non-recovering readers while holding the shared lock.

Tests compare recursive names, bytes, modes, mtimes and sizes before/after (atime excluded), cover missing lock and symlink/inode replacement, and run thread/subprocess writer-vs-reader barriers proving either a coherent before/after snapshot or bounded `snapshot_unstable`, never torn evidence.

## 2. Installed promotion-policy resource

### Files
- new `checkin_cli/policies/__init__.py`
- `checkin_cli/policies/diagnostic-promotion-policy.json`
- `pyproject.toml` package-data configuration
- `diagnostic_evidence.py` embedded expected SHA-256 and `importlib.resources.files("checkin_cli.policies")` loader
- profile packaging tests.

`pyproject.toml` explicitly includes `checkin_cli.policies/*.json` in wheel/sdist package data. Runtime loads only through `importlib.resources`, canonicalizes exact bytes, and compares to the code-embedded digest before deriving any promotion trust root. Missing/mismatched resource disables manifest generation.

A hermetic test builds a wheel into a temporary directory with the project’s configured build backend, inspects wheel members, installs/extracts into an isolated temp target without network/dependency resolution, imports the package resource, and verifies bytes and digest. Source-tree-only success is insufficient.

## 3. Detachment/provider linearization fence

### State contract
Add nonterminal `detaching` with monotonically increasing `session_generation`. `DiagnosticHost` owns one async admission lock shared by `activate/revalidate`, `begin_delivery_admission`, `detach/close/expiry/stop`, and route generation snapshots.

- Detach acquires admission lock, validates generation, atomically transitions active→detaching in the durable session journal, increments generation, marks in-memory host detaching, and removes routes before releasing the lock.
- Final delivery admission acquires the same lock and, in the same critical section as durable reservation creation, verifies state=active, exact generation/session/boot/full pins and route full triple. Reservation receives that generation.
- If reservation linearizes first, it is an admitted in-flight attempt governed by existing at-most-once and post-reservation revalidation; detach waits for the bounded in-flight admission counter to reach zero before terminal cleanup. If detach linearizes first, later reservation/provider count is zero.
- Immediately before provider call, attempt rechecks durable state/generation under the same admission protocol. A detaching mismatch terminalizes the existing reservation with provider zero.
- Stop/expiry use the same detach method; cancellation cannot skip durable detaching/cleanup. Crash recovery treats detaching as routes absent, provider forbidden, and completes disable/terminal cleanup.

`DiagnosticRoleRoute` stores the exact full triple. `_lookup_diagnostic_role_route` compares sender `user_id`, `chat_id`, and `topic_id`, then carries the session generation to final admission. Any mismatch yields product/provider row zero.

Tests cover route lookup paused before admission while close/expiry/stop wins; admission paused before reservation; reservation-first then detach; provider-boundary generation change; thread/event-loop/subprocess equivalents where applicable. Assertions distinguish diagnostic session/transition rows from nutrition/delivery rows and prove no provider call after the detaching linearization point.

## Pre-mortem
1. Reader creates a missing lock: blocked by O_RDONLY and missing-lock rejection.
2. Wheel omits policy: build/install resource test blocks release.
3. Handler retains stale route through close: generation carried to final admission rejects.
4. Reservation races detach: shared admission lock defines one winner and provider semantics.

## Verification
Profile cwd `/home/cube/.hermes/profiles/physique-coach/workspace/checkin_cli`:
- `.venv/bin/python -m pytest -q`
- `.venv/bin/python -m compileall -q checkin_cli`
- wheel/resource test is part of full pytest and must run offline.

Gateway cwd `/home/cube/projects/richard/hermes-agent`:
- `.venv/bin/python -m pytest -q tests/gateway/test_adaptive_nutrition.py tests/gateway/test_nutrition_coaching.py tests/gateway/test_telegram_physique_checkin.py tests/gateway/test_telegram_group_gating.py`
- `.venv/bin/python -m py_compile gateway/platforms/diagnostic_isolation.py gateway/platforms/nutrition_coaching.py gateway/platforms/telegram.py gateway/platforms/nutrition_coaching_config.py`
- full `tests/gateway` run and truthful classification.

No network/live Telegram/manual P2–P6/deployment. Final cleaner, Architect, executor QA/red-team and terminal Critic are mandatory.

## Acceptance
1. Snapshot success and rejection leave source tree unchanged except atime.
2. Reader/writer synchronize on the same verified inode; torn snapshot impossible.
3. Built/installed package exposes exact policy bytes through importlib.resources.
4. After durable detaching fence, new nutrition/delivery reservation and provider calls are zero.
5. A pre-fence admitted reservation remains at-most-once and is revalidated before provider.
6. Route authorization compares full triple and generation.
7. All Stage-28 contracts and human-only boundaries remain unchanged.

## Intent reconciliation
The user approved automatic Ultragoal execution after these three safety details reach consensus. No additional product scope or Telegram UI is introduced.

## ADR
Decision: shared-inode non-mutating read APIs, digest-verified packaged policy resource, and durable detaching generation fence. Alternatives rejected because they permit torn evidence, missing policy, or post-close sends. Consequence: a small amount of lock/packaging code and focused race tests; production behavior remains fail-closed.
