# 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.

# Revision 30 Binding Addendum
This addendum is normative and changes only the lock-domain, provider linearization, and package digest details of Stage 29.

## Exact evidence lock domains
No reader acquires two subsystem locks simultaneously. The exporter reads one source under its matching shared lock, releases it, then moves to the next; the complete before/after revision-token pass detects cross-source changes.

| Evidence source | Read API | Existing writer inode | Data covered |
|---|---|---|---|
| wizard draft | `WizardStorage.read_locked()` | `WizardStorage._lock` = `.wizard.lock` | one validated selected draft JSON |
| finalized wizard/canonical profile event | `EventStore.read_locked()` in `store.py` | EventStore `.events.lock` | validated event lookup/event JSONL owned by EventStore |
| adaptive lifecycle/delivery/source/authority/config/overlay | `AdaptiveEventStore.read_adaptive_locked()` | adaptive root `.adaptive.lock` | `path`, `source_day_path`, `source_intent_path`, `authority_path`, `config_epoch_path`, `overlay_path` |
| canonical adaptive event/sequence | `CanonicalSequenceJournal.read_locked()` | canonical root `.events.lock` | `canonical_events_path`, `canonical_sequence_path` |
| schedule | `schedule_delivery_read_lock(...)` | `.scheduled-deliveries.lock` | validated schedule ledger/fence/tombstone inventory |

Each new reader uses O_RDONLY|O_NOFOLLOW|O_CLOEXEC, regular-file/0600/owner/link-count/inode verification, LOCK_SH, non-recovering validators, and no filesystem mutation. Corresponding writers open the same initialized inode with O_RDWR|O_NOFOLLOW|O_CLOEXEC and LOCK_EX. Missing/unsafe locks fail closed. Concurrent tests exist for all five domains. The incident map calls only the table API associated with each row; canonical adaptive evidence never uses `.adaptive.lock`, and EventStore evidence never uses WizardStorage’s lock.

## Provider-call linearization
Choose the conservative diagnostic-only design: **hold `DiagnosticHost._admission_lock` from final generation/state/pin validation through reservation, provider invocation, durable receipt/unknown terminalization, and completion of the bounded provider call**. The diagnostic provider call has a hard configured timeout no greater than the remaining session TTL and an absolute test-safe maximum. This intentionally serializes diagnostic sends; operational throughput is irrelevant in the isolated single-owner environment.

Linearization:
- If delivery acquires the lock first, its provider attempt is pre-detach. Close/expiry/stop cannot durably append `detaching` until the attempt returns/throws/times out and its receipt or terminal unknown is durable.
- If detach acquires first, it durably appends `detaching`, increments generation and removes routes while holding the same lock; every later admission rejects with reservation/provider zero.
- There is no in-flight counter or lease outside the lock and therefore no check-to-call gap.

All provider invocation and terminal persistence is inside `async with _admission_lock` with `try/except/finally`. Cancellation is caught, the attempt is terminalized unknown when no durable receipt exists (or audit-pending when receipt exists), then cancellation is re-raised only after state persistence; the lock is released by `finally`. A process crash is recovered from the reservation/receipt ledger using existing no-resend rules before any restart revalidation. Detach waiting on the lock has a bounded host shutdown timeout; timeout does **not** append detaching out of order or force a second send—it leaves the host unavailable, cancels the provider task through the same terminalization path, then retries lock acquisition and cleanup.

Deterministic tests pause: (a) immediately before provider invocation while admission lock is held and assert close has not yet appended detaching; then allow provider terminalization and close; (b) let close acquire first and assert reservation/provider zero; (c) cancel/timeout during provider and assert one terminal row, no retry, then close; (d) restart from reservation/receipt crash states and assert no resend.

`DiagnosticRoleRoute` full-triple `user_id/chat_id/topic_id` comparison and generation propagation remain mandatory.

## Package digest/install scope
The embedded promotion-policy digest is SHA-256 over the **raw bytes returned by `importlib.resources`**, compared before JSON parsing. Only after a byte match is the resource decoded and validated against its closed JSON schema. The release artifact claim is explicitly limited to the wheel used for installation; no sdist-survival claim is made. The hermetic wheel build/inspection/temp-target import test is mandatory and network-disabled.

## Acceptance additions
- Every evidence read synchronizes with the actual writer inode listed above and never holds two subsystem locks.
- Durable `detaching` can never precede the start or completion of a provider attempt that passed admission; detach and the full bounded attempt are mutually exclusive on one lock.
- The installed wheel contains exact raw policy bytes matching the embedded SHA before parsing.


# Revision 31 Binding Addendum
This addendum resolves the remaining Stage-30 findings.

## Overlay and canonical lock corrections
Remove `overlay_path` from `AdaptiveEventStore.read_adaptive_locked()`. Add `OverlayJournal.read_locked()` using the existing `root/.authority-transition.lock` writer inode with O_RDONLY|O_NOFOLLOW|O_CLOEXEC, regular-file/0600/owner/link/inode checks, LOCK_SH, non-recovering validation and no filesystem mutation. Harden the matching OverlayJournal writer open on that same initialized inode with O_RDWR|O_NOFOLLOW|O_CLOEXEC and LOCK_EX. Add a dedicated concurrent overlay writer-vs-reader coherence test and missing/symlink/inode-replacement cases.

Canonical evidence is split into two sequential domains:
- canonical event bytes at `customer_root/wizard/events.jsonl` are read only through `EventStore.read_locked()` on `wizard/.events.lock`;
- canonical sequence bytes are read only through `CanonicalSequenceJournal.read_locked()` on `adaptive_root/.events.lock`.
The exporter never treats one lock as protecting both files, never holds both, and uses before/after tokens plus retry to validate their relation.

The final source-lock table therefore has seven explicit domains: WizardStorage draft, EventStore canonical event, AdaptiveEventStore main/source/authority/config, CanonicalSequenceJournal sequence, OverlayJournal overlay, customer schedule, and profile authority/spec documents. Each uses its actual writer inode.

## Exact diagnostic provider timeout/cancellation
Extend `DiagnosticIsolationSpecV1` with `max_provider_timeout_seconds`, integer 1..30, included in `spec_digest` and all diagnostic capabilities. Preflight requires it valid. Effective timeout at admission is `min(max_provider_timeout_seconds, floor(expires_at_kst-now))`; a non-positive result rejects before reservation.

Add `DiagnosticHost.deliver_with_admission(...)`. It acquires `_admission_lock`, validates session/generation/full pins, then enters `asyncio.timeout(effective_timeout)` and calls the existing coordinator delivery path with internal keyword `propagate_cancel=True`. The lock remains held through reservation, transport/provider call and durable terminal result.

The coordinator’s existing cancellation handler keeps ordinary behavior by default. With `propagate_cancel=True`, it first persists exactly one linked `delivery_unknown` when no durable receipt exists or preserves/appends `audit_pending` when a receipt exists, then re-raises `CancelledError`. No resend is scheduled. `asyncio.TimeoutError` is converted after the same terminal persistence. `DiagnosticHost` releases `_admission_lock` only after that persistence via `finally`; close/expiry/stop then acquires it and appends detaching/cleanup. The parameter is internal and accepted only with a validated diagnostic capability/session; production callers cannot select it.

Tests pin 1-second deterministic fake-clock/fake-provider timeouts, cancellation immediately before provider, during provider, and after receipt, asserting one provider maximum, one terminal chain, no retry, cancellation/timeout propagation to host, then successful detaching cleanup. Spec/capability timeout tampering rejects before rows/provider.


# Revision 32 Binding Addendum
This addendum resolves every Stage-31 finding.

## Canonical event/sequence single writer domain
Refactor all canonical event/sequence writes, including `AdaptiveEventStore.append_canonical_event`, to a single `CanonicalSequenceJournal` transaction whose `lock_path` is **the canonical event parent’s `.events.lock`** (`customer_root/wizard/.events.lock`). `EventStore` already uses that inode. The journal acquires it with LOCK_EX, validates/appends the canonical event and sequence under one lock, and fsyncs both. No writer may append either canonical file under `adaptive_root/.events.lock` after migration.

Add a compatibility startup assertion that no active writer/callsite constructs `CanonicalSequenceJournal` for those files with another lock path. Existing adaptive-root `.events.lock` is not deleted or repurposed; it simply ceases to authorize canonical event/sequence writes. `EventStore.read_canonical_locked()` uses wizard `.events.lock` and reads both event and sequence sequentially under that one shared lock. Deterministic tests run ordinary EventStore append, AdaptiveEventStore canonical append and reader barriers and prove coherent pairs.

## Domain-owned recovery only
Remove direct canonical-event, canonical-sequence and overlay truncation from generic `AdaptiveEventStore.recover(path=None)`. Generic recovery may recover only main/source/authority/config journals while already holding `.adaptive.lock`.

Add:
- `CanonicalSequenceJournal.recover_locked()` under wizard `.events.lock`, repairing event/sequence together and validating their relation;
- `OverlayJournal.recover_locked()` under `.authority-transition.lock`;
- read-only counterparts on the same inodes.

Every public recovery call dispatches to the owning journal API; no raw cross-domain `_recover_jsonl` call is exposed. Recovery-vs-reader and append-vs-reader tests cover canonical and overlay domains. The final evidence lock domains are: wizard draft, canonical event+sequence (wizard `.events.lock`), adaptive main/source/authority/config (`.adaptive.lock`), overlay (`.authority-transition.lock`), schedule, profile authority/spec.

## Enforceable diagnostic transport boundary
Define `DiagnosticDeadlineTransport` protocol with async-only `send_adaptive_customer(..., *, deadline_monotonic: float)`; required immutable attributes `diagnostic_transport_v1=True`, `cancellation_cooperative=True`, and `max_provider_timeout_seconds<=30`. `DiagnosticHost` accepts only the concrete registered test `TelegramCustomerTransport` adapter implementing this protocol or a test double explicitly satisfying it. `inspect.iscoroutinefunction` and protocol attributes are validated **before reservation**; synchronous callables and generic compatibility transports reject with all row/provider deltas zero.

`TelegramCustomerTransport` passes the remaining deadline into the Telegram client/request timeout so the provider-native network operation is bounded. The host uses `asyncio.timeout(effective_timeout)` around the async call while holding `_admission_lock`. Effective timeout is min(spec 1..30 seconds, transport maximum, remaining session TTL). A non-positive value rejects before reservation.

The diagnostic path disables the ordinary sync/awaitable compatibility branch. With `propagate_cancel=True`, cancellation/timeout terminalizes one unknown when no receipt exists or audit-pending when a receipt exists, then re-raises. A transport that lacks the cooperative/deadline contract—including a cancellation-resistant fake—is rejected before reservation/provider; it is never started and therefore cannot send later. Tests cover blocking synchronous, awaitable-but-unmarked and cancellation-resistant nonconforming transports as pre-reservation rejection/provider zero, plus a conforming cooperative transport cancelled before/during/after receipt with terminal persistence and no resend.

## Acceptance additions
- Canonical event and sequence have exactly one writer lock inode across every append/recovery path.
- Generic recovery cannot mutate canonical or overlay files.
- Only deadline-aware cancellation-cooperative async test transport can enter diagnostic reservation/provider flow; all compatibility transports fail before reservation.


# Revision 33 Terminal Binding Addendum
This final addendum resolves Stage-32 transaction, recovery and transport-instance findings.

## Canonical event transaction owner
Add `CanonicalEventTransaction` in `store.py`. Constructor requires `events_path` and `sequence_path`; it derives the only lock path as `events_path.parent / ".events.lock"`, rejects symlinks/non-private paths and has no lock override. The authoritative sidecar for a customer is exactly `customer_root / "nutrition-plans" / "canonical-sequence.jsonl"`; all callsites derive it from the typed customer runtime/data-root contract, never caller strings.

Public methods `append_one`, `append_many`, `recover`, `read_snapshot` acquire wizard `.events.lock` exactly once. Private `_append_one_locked`, `_append_many_locked`, `_recover_locked`, `_read_snapshot_locked` require an internal unforgeable lock token and never flock again. Under one LOCK_EX transaction they preserve current EventStore dedupe/import ordering, append/fsync event bytes, append/fsync matching sequence rows, validate the pair and rebuild projections as applicable. Batch import validates the complete batch before any append and appends each event/sequence pair under the same lock.

`EventStore` accepts an optional typed `CanonicalEventTransaction` at construction. When present, `_record_lock` and batch/history imports delegate to its public transaction and do not acquire a second lock; internal EventStore work uses the lock token. When absent, existing ordinary event-only behavior remains but may not be used for adaptive canonical customer events. `AdaptiveEventStore.append_canonical_event`, reconciliation, freeze/legacy-prefix and canonical validation all delegate to the same transaction; `CanonicalSequenceJournal` becomes a private compatibility facade over the transaction and rejects any supplied lock path not equal to wizard `.events.lock`. Old adaptive-root lock selection is removed from every callsite.

Existing sidecar bytes at the authoritative nutrition-plans path are validated in place; no path migration occurs in v1. A legacy-prefix digest/count mismatch fails closed. Tests cover single append, batch/history import, concurrent EventStore/adaptive append, recovery, constructor lock mismatch and no nested flock.

## Public/private recovery ownership
- `AdaptiveEventStore.recover()` public wrapper acquires `.adaptive.lock` once and calls `_recover_adaptive_locked(token, targets)`; allowed targets are only main, source-day, source-intent, authority and config journals. Overlay/canonical/arbitrary paths are rejected.
- `read(recover=True)`, `journal_rows(recover=True)`, `source_day_rows(recover=True)` call the public wrapper before their nonrecovering read, or use an internal token when already locked; no nested flock.
- `CanonicalEventTransaction.recover()` owns event+sequence repair under wizard `.events.lock`.
- `OverlayJournal.recover()` owns overlay repair under `.authority-transition.lock`.
- schedule recovery remains under its schedule lock.

Every private locked method accepts a module-private object-identity token created only by its context manager. All current `recover=True` callsites are enumerated and migrated. Recovery tests cover public and already-locked paths, arbitrary/cross-domain rejection and deadlock-free concurrency.

## Immutable sealed diagnostic transport
`DiagnosticHost.__init__` receives and permanently stores one exact transport instance. Production construction accepts only `type(transport) is TelegramCustomerTransport`, exact expected adapter object identity, exact verified test-bot identity and the sealed diagnostic method implemented by that class. The instance is captured in the active session/spec digest and rechecked by object identity under `_admission_lock`; coordinator `set_customer_transport` is disabled/rejected while a diagnostic host/session is prepared, active, detaching or restart-unvalidated. Delivery passes the frozen instance explicitly from reservation through provider invocation; it never rereads mutable coordinator state.

The exact method is:
`async send_diagnostic_customer(body, *, reservation_id: str, deadline_monotonic: float, destination_digest: str) -> Mapping[str, object]`.
It computes remaining monotonic seconds and passes that bound to Telegram’s native `send_message` kwargs `connect_timeout`, `pool_timeout`, `write_timeout`, and `read_timeout`; each is positive and no greater than the effective deadline. It validates reservation/destination/session pins before the network call.

Test doubles cannot enter the production constructor. Tests use `DiagnosticHost._for_test(..., transport, test_token)` where `test_token` is a module-private sentinel available only through a test helper in the same module under explicit `shadow_test_only`; production config cannot set it. The test transport must subclass a sealed internal test adapter and implement the same async signature. A duck-typed object with marker attributes, a synchronous callable, wrong adapter, concurrent setter swap, or cancellation-resistant arbitrary coroutine is rejected before reservation/provider. Tests verify transport identity remains constant through the call and setter races fail.

The admission lock remains held through frozen-instance reservation, deadline-aware provider call and durable terminalization as established in Revision 31. Native request timeouts plus asyncio timeout bound the actual Telegram request; cancellation persists unknown/audit-pending then re-raises.

## Acceptance additions
1. Every adaptive canonical writer/recovery call shares one wizard `.events.lock` transaction with no nested flock.
2. Public recovery acquires exactly one owner lock and cannot mutate another domain.
3. Diagnostic transport identity is immutable from host construction through provider completion; production admits only the exact trusted Telegram transport/adapter pair.


---

# Final Constructor and Replay-Safety Closures

# Diagnostic Isolation — Constructor Closure Plan

## Scope
Incorporates Stage 33 and changes only three construction contracts. User approved Ultragoal execution after consensus.

## RALPLAN-DR
Principles: durable identity differs from process identity; registered adaptive paths must be impossible to construct without paired canonical persistence; dormant control must not require an enabled coordinator. Drivers: restart safety, constructor-enforced invariants, testability. Chosen option is explicit typed factories and sealed bindings. Duck typing, ambient globals and optional production fallbacks are rejected.

## 1. Canonical transaction factory and enforcement
Add `CanonicalEventTransaction.for_customer_runtime(runtime: CustomerRuntime)` in `store.py`. `CustomerRuntime` exposes canonical resolved `customer_root`, `wizard_root`, and `nutrition_plans_root`; factory derives:
- events: `wizard_root/events.jsonl`
- sequence: `nutrition_plans_root/canonical-sequence.jsonl`
- lock: `wizard_root/.events.lock`
It validates containment/privacy/no symlinks and returns the only registered-customer transaction.

`WizardService.__init__(wizard_root, *, canonical_transaction=None, adaptive_registered=False)` enforces `adaptive_registered => canonical_transaction is not None` before creating `EventStore`. `NutritionCoachingCoordinator._configure_registry` builds the transaction from each enabled `CustomerRuntime` and passes both arguments. Customer admin activation/reconciliation and every adaptive wizard construction use the same factory. `EventStore` receives the transaction and registered finalization calls paired transaction append; if absent in a registered/adaptive flow it rejects before event bytes. Standalone nonregistered `WizardService`/`EventStore` retains event-only behavior.

Tests: registered customer cannot construct/finalize without transaction; exact paths/lock; coordinator/admin/reconcile all use factory; standalone unchanged; batch/single append paired; no nested flock.

## 2. Dormant delivery authority and transport factory
Add `DiagnosticDeliveryAuthority` independent of any coordinator. Constructor takes verified spec, boot epoch, profile session store, registry/artifact loaders and immutable adapter/test-bot identity. Methods:
- `verify_reservation(session_digest, generation, reservation_id, destination_digest, deadline)`
- `verify_provider_start(...)`
- `terminalize_cancel_or_timeout(...)`
Every method reloads current session/spec/owner/registry/bot/pins under the established admission/profile locks.

Add `TelegramCustomerTransport.for_diagnostic(adapter, verified_spec, authority)`. It requires the dormant adapter’s exact verified bot identity, exact customer destination from spec, sealed diagnostic method version, and native Telegram async sender. It does not require `NutritionCoachingCoordinator`. It returns a transport whose only diagnostic method is:
`async send_diagnostic_customer(body, *, reservation_id, session_digest, generation, deadline_monotonic, destination_digest)`.
The method delegates authority verification then calls the exact adapter bot with bounded connect/pool/write/read timeouts. Generic send compatibility is unavailable on this object.

Construction order: validate roots/spec/bot → create session store and `DiagnosticDeliveryAuthority` → create sealed diagnostic transport → create `DiagnosticHost` → create dormant `DiagnosticControlService` → register Topic-59 only. Activation later creates the child coordinator and injects the already frozen transport explicitly. No circular coordinator dependency.

## 3. Durable transport binding digest
Define canonical preimage:
`{schema_version:"diagnostic_transport_binding_v1", adapter_kind:"telegram-test-bot", test_bot_digest, customer_destination_digest, max_provider_timeout_seconds, method_version:"send_diagnostic_customer_v1", authority_spec_digest}`.
`diagnostic_transport_binding_digest = sha256(canonical_json(preimage))`.

The digest is computed before session prepare from verified durable inputs and stored in spec approval/config projection, every diagnostic session/transition/capability/reservation row, and preflight digest map. It never contains Python object identity. On restart the factory reconstructs authority/transport from the same verified spec and adapter identity and requires exact digest equality before Topic-59 prepare/revalidate. Changed bot/destination/timeout/method/spec invalidates old sessions.

`DiagnosticHost` additionally freezes the process-local transport reference and checks `transport is self._transport` under admission lock from reservation through provider completion. This is an extra runtime TOCTOU check only. Coordinator setters reject while diagnostic state is nonterminal.

Tests: digest vectors; same-value restart rebind succeeds only current boot revalidation; any preimage mutation rejects; object swap with same marker/digest rejects by identity; old process object is never serialized; dormant factory requires no coordinator; setter race provider zero.

## Pre-mortem and verification
- Missing transaction writes unpaired event: constructor/finalization rejection.
- Dormant transport accidentally needs coordinator: constructor test with no coordinator.
- Restart trusts object identity: digest-vector/reconstruction tests.
- Swap after validation: frozen-reference admission test.

Run full profile pytest+compileall; focused gateway tests+py_compile including diagnostic sibling; full gateway truthfully; wheel/policy and read-lock/race tests from Stage 33; cleaner/Architect/QA/Critic. No live Telegram, credentials, manual P2–P6 or deployment.

## Acceptance
All registered adaptive wizard events are paired by factory construction; dormant host builds without coordinator; durable binding is canonical/restartable and process identity is only an additional check; all Stage-33 contracts remain unchanged.

## Intent reconciliation
No new user choice: this only makes the previously approved isolated diagnostic design constructible and restart-safe.

## ADR
Use typed factories and a durable binding digest; reject optional production fallbacks and process-object persistence.

# Revision 35 Binding Addendum
This addendum resolves every Stage-34 finding.

## Sealed registered versus standalone construction
Add frozen `RegisteredCustomerBinding` to `customer_coaching.py`, created only by committed/diagnostic registry loaders with a module-private sentinel. Fields: customer key digest, resolved data-root digest, registry digest/version, activation digest, mode, binding digest, and nonserializable private sentinel. `CustomerRuntime` from a registry always carries it.

`WizardService` and `EventStore` have no permissive public constructor defaults. Their internal constructors require a module-private construction token. Public factories are disjoint:
- `WizardService.for_registered(runtime: CustomerRuntime)` requires and validates `RegisteredCustomerBinding`, builds `CanonicalEventTransaction.for_customer_runtime(runtime)`, and constructs registered EventStore.
- `WizardService.for_standalone(home)` explicitly creates event-only standalone mode and rejects a `CustomerRuntime`/registered binding.
- `EventStore.for_registered(transaction, binding)` and `EventStore.for_standalone(home)` mirror this split.

Direct `WizardService(home)`, `EventStore(home)`, omitted transaction/binding, forged binding, or standalone factory passed a registered runtime reject before opening/appending bytes. Every production registered callsite in gateway coordinator, wizard handlers, customer admin, reconciliation, activation and tests uses `for_registered`. Explicit standalone tools/tests use `for_standalone`. Loader absence/corruption rejects before service construction. Tests assert direct bare construction and omitted binding cannot append, while standalone remains compatible through its explicit factory.

## Exact diagnostic delivery bridge
Add typed `DiagnosticDeliveryRequest(session_digest, generation, reservation_id, destination_digest, transport_binding_digest)`; it contains no body. Add frozen `VerifiedDiagnosticReservation` containing canonical reserved body bytes, body digest, exact destination binding, reservation row digest, receipt pin, session/generation/binding digests, and a module-private verification token.

`DiagnosticDeliveryAuthority` exact methods, all requiring the host’s private admission-lock token:
- `verify_reservation(request, *, lock_token) -> VerifiedDiagnosticReservation`: reload session/spec/owner/registry/reservation, require active/current generation/binding, verify reservation row and immutable body/body digest, and return canonical reserved bytes. Missing/stale/conflict raises `DiagnosticAuthorityError` before provider.
- `verify_provider_start(verified, *, deadline_monotonic, lock_token) -> VerifiedDiagnosticReservation`: revalidate every live pin, exact reservation/body/destination/binding and positive deadline; return the same verified object only if unchanged.
- `terminalize_cancel_or_timeout(verified, *, receipt, reason, lock_token) -> Mapping`: append exactly one linked unknown when receipt absent or audit-pending when receipt present; idempotently return existing exact terminal, conflict fails closed.

Add `DiagnosticHost.deliver(request)`. While holding `_admission_lock`, it calls verify_reservation, creates/validates durable admission as required by Stage 33, calls verify_provider_start, then calls the frozen transport directly with `VerifiedDiagnosticReservation`—never the ordinary `send_adaptive_customer` discovery branch. Transport signature:
`async send_diagnostic_customer(verified: VerifiedDiagnosticReservation, *, deadline_monotonic: float) -> Mapping`.
It sends only `verified.body_bytes` to `verified.destination_binding`; caller-supplied body/destination is impossible. Host persists receipt/audit or uses terminalize method on timeout/cancellation before releasing lock. Activated child coordinator delegates diagnostic send to `DiagnosticHost.deliver`; generic customer transport path is prohibited in diagnostic mode.

Integration tests cover exact successful argument propagation; stale reservation/generation/body/destination/authority/binding provider zero; cancellation/timeout terminal once; close race; ordinary generic sender never called.

## Acyclic digest schema
Amend `DiagnosticIsolationSpecV1` with fields `spec_core_digest` and `diagnostic_transport_binding_digest`. Canonical hash order:
1. `spec_core_preimage` = all durable spec inputs except `spec_core_digest`, `diagnostic_transport_binding_digest`, `spec_digest`, `authority_digest`, and approval metadata. `spec_core_digest=SHA256(canonical_json(spec_core_preimage))`.
2. transport preimage = `{schema_version:"diagnostic_transport_binding_v1", adapter_kind:"telegram-test-bot", test_bot_digest, customer_destination_digest, max_provider_timeout_seconds, method_version:"send_diagnostic_customer_v1", spec_core_digest}`. Hash to `diagnostic_transport_binding_digest`.
3. `spec_digest=SHA256(canonical_json({spec_core_digest, diagnostic_transport_binding_digest}))`.
4. `authority_digest=SHA256(canonical_json({spec_digest, approved_by, approved_at_kst, supersedes_digest}))`.

The owner-approved spec/config projection stores all four digests and exact schema versions. Every session/transition/capability/reservation stores spec, authority and transport-binding digests. On restart the transport factory recomputes steps 1–4 from verified bytes and current adapter identity, requiring exact equality before revalidation. Python object identity is never serialized; `is` remains an additional process-local check.

Golden vector tests parse fixed raw spec bytes and independently recompute all four digests, reject field/exclusion/order mutation, and prove no cycle or object representation enters canonical JSON.

## Acceptance additions
- Registered and standalone persistence are reachable only through disjoint sealed factories; no optional fallback exists.
- Diagnostic provider receives only authority-returned reservation-pinned bytes/destination through `DiagnosticHost.deliver`.
- Digest derivation is ordered, acyclic, byte-reproducible and restart-safe.


# Revision 36 Binding Addendum
This addendum resolves the sole Stage-35 Architect finding.

## Host-owned reserve-and-verify sequence
Replace `DiagnosticDeliveryRequest` with `DiagnosticDeliveryCandidate`, created only by the activated child coordinator from the exact latest approved/activated proposal and persisted rendered body. Fields: session/spec/transport-binding digests, generation, proposal/revision/body/destination/config/registry/consent/activation/source/registration/policy/catalog/constraints/epoch pins, dedupe key, and canonical body bytes+digest. It is not a reservation and grants no provider authority.

Replace `verify_reservation` with:
`DiagnosticDeliveryAuthority.reserve_and_verify(candidate, *, lock_token) -> VerifiedDiagnosticReservation`.

While `DiagnosticHost.deliver(candidate)` holds `_admission_lock`, `reserve_and_verify`:
1. reloads active session/generation/spec/owner/registry and every authority/artifact/proposal pin;
2. recomputes persisted rendered body digest and destination binding, rejecting any candidate mismatch before rows;
3. under the existing adaptive store lock appends the immutable `delivery_attempt_started` reservation with all pins/body digest and obtains reservation ID;
4. rereads and validates the exact committed reservation;
5. returns `VerifiedDiagnosticReservation` containing only reservation-pinned canonical bytes/destination and private verification token.

No reservation can be created outside this method in diagnostic mode. The ordinary coordinator reservation branch is bypassed; activated diagnostic coordinator calls only `DiagnosticHost.deliver(candidate)`. If an exact terminal/consumed reservation already exists, method returns the typed duplicate/no-send result and never creates provider authority. Conflicting same dedupe key fails closed.

`DiagnosticHost.deliver` order under one admission lock is exact: `reserve_and_verify` → `verify_provider_start` → frozen transport `send_diagnostic_customer(verified, deadline)` → persist receipt/delivered/audit terminal; timeout/cancellation uses `terminalize_cancel_or_timeout`; finally release lock. Close/expiry/stop uses the same lock. Therefore close-before-reserve yields all delivery/provider deltas zero; reserve-first is a pre-detach attempt and detaching cannot append until its terminal row is durable.

Tests deterministically cover: candidate validation failure/no row; close wins before reserve/no row/provider; reserve wins then close waits; crash immediately after reservation/restart no resend; duplicate candidate no row/provider delta; body/destination/pin mismatch before reservation; exact successful one provider chain.


# Revision 37 Binding Addendum
This addendum resolves every Stage-36 Critic finding.

## Closed reservation decision algebra
`reserve_and_verify(...)` returns the sealed union:
- `VerifiedDiagnosticReservation` — fresh provider authority;
- `DiagnosticDuplicateNoSend` — stable fields `status="duplicate"`, existing terminal/status digest, Korean/operator outcome, provider_authority=false;
- `DiagnosticUnknownNoSend` — stable fields `status="delivery_unknown"`, linked terminal row digest, provider_authority=false.

`DiagnosticHost.deliver` pattern-matches immediately. Only `VerifiedDiagnosticReservation` proceeds to `verify_provider_start` and transport. Duplicate/unknown returns the stable result with no provider and no later branch.

## Existing dedupe-state matrix
Under admission+adaptive store locks:
- no row: validate, append one started reservation, reread, return Verified;
- exact unconsumed `delivery_attempt_started`: append/reuse exactly one linked `delivery_unknown` reason `diagnostic_started_without_provider_receipt`, return DiagnosticUnknownNoSend; never recreate authority;
- exact consumed row without receipt/terminal: append/reuse linked unknown, return UnknownNoSend;
- exact receipt/delivered/audit-pending/sent-audited/unknown terminal chain: return DiagnosticDuplicateNoSend using canonical existing status; no row/provider delta except reconciliation is available only through its separate capability/action;
- any same-dedupe body/destination/session/generation/spec/binding/pin conflict: raise `DiagnosticReservationConflict`, no new row/provider.

Startup recovery runs before diagnostic revalidation and applies the same matrix to all exact started/consumed rows, terminalizing uncertain rows unknown with provider zero. Therefore restart can never convert persisted reservation evidence into provider authority.

Tests cover every matrix row with exact row/provider deltas, restart ordering, and stable caller-visible result text/status.

## Mechanical host-only reservation ownership
`RegisteredCustomerBinding.mode` distinguishes ordinary versus `diagnostic_isolated_v1`. At the first line of public ordinary `AdaptiveNutritionCoordinator.deliver_latest_once` and every ordinary reservation helper, a diagnostic-bound runtime raises `AdaptiveWorkflowError("diagnostic delivery requires DiagnosticHost")` before store lock/append/provider. No compatibility fallback or raw capability bypass applies.

The diagnostic host uses a separate private `_deliver_diagnostic_candidate(candidate, host_lock_token)` path that validates the module-private host token and calls only `reserve_and_verify`. It is not exported. Direct invocation of ordinary delivery/reservation against diagnostic runtime is tested for zero session-external lifecycle/delivery rows and provider zero; forged host token rejects. Code search/AST tests assert `delivery_attempt_started` for diagnostic mode is emitted only from `DiagnosticDeliveryAuthority.reserve_and_verify`.

## Acceptance additions
Fresh authority is created once only; persisted started/consumed evidence is terminal no-send; duplicate/terminal evidence is stable no-send; ordinary public delivery cannot reserve for diagnostic runtimes.


# Revision 38 Binding Addendum
This addendum resolves the sole Stage-37 Critic finding.

## Canonical terminal no-send variants
The sealed union is:
- `VerifiedDiagnosticReservation` (`provider_authority=true`);
- `DiagnosticUnknownNoSend(status="delivery_unknown", text="전송 결과를 확인할 수 없습니다. 다시 보내지 마세요. 조정이 필요합니다.", reconciliation_available=false)`;
- `DiagnosticAuditPendingNoSend(status="audit_pending", text="고객 전송 영수증은 확인됐습니다. 재전송하지 말고 감사 기록을 복구해 주세요.", reconciliation_available=true)`;
- `DiagnosticDuplicateNoSend(status="duplicate", text="이미 처리된 전송입니다.", reconciliation_available=false)`.
All no-send variants have provider_authority=false.

Validated chain precedence:
1. A valid chain containing `sent_audited` returns DuplicateNoSend, even if earlier rows include receipt/delivered/audit_pending.
2. Otherwise a valid chain with provider receipt, delivered, or audit_pending returns AuditPendingNoSend; reconciliation capability is the only allowed next mutation and provider stays zero.
3. Otherwise an existing `delivery_unknown` returns UnknownNoSend.
4. Otherwise exact started/consumed without receipt is terminalized once to unknown and returns UnknownNoSend.
5. Receipt evidence mixed with unknown without the canonical audit-pending normalization, multiple conflicting receipts/terminals, or invalid ordering fails closed as `DiagnosticReservationConflict`, no row/provider.

`DiagnosticHost.deliver` returns these variants unchanged before provider-start branching. Reconciliation transforms a valid audit-pending chain to `sent_audited`; the next repeated send returns DuplicateNoSend. It never transforms unknown into send permission.

Matrix tests assert exact result class, status, Korean text, row delta, provider delta and reconciliation availability for started, consumed, unknown, receipt-only, delivered-without-audit, audit-pending, audit-pending→sent-audited, and direct sent-audited chains across same-process replay and restart recovery.


## Final Intent Reconciliation
- The canonical owner reuses one Telegram user across operator, test-customer, and test-trainer roles only in the isolated diagnostic process; full triples/destinations remain distinct.
- Ordinary production behavior and existing identity semantics remain unchanged.
- Problems are raised and fixes requested in this development session; no natural-language Telegram diagnostic UI is added.
- Live Telegram, account/token handling, manual P2–P6, deployment, and real-customer activation remain human-only.
- The user explicitly approved automatic Ultragoal implementation after consensus.

## Final ADR
**Decision:** Separate dormant test-bot diagnostic process with owner-authenticated sessions, sealed registered persistence, coherent read-only evidence, synthetic replay, packaged promotion policy, frozen deadline-aware transport, and host-owned replay-safe reservation authority.

**Drivers:** solo-owner usability, zero production bypass, deterministic incident reproduction, no-resend safety.

**Alternatives:** permanent master bypass, mandatory three accounts, hot-attached production adapter, ambient/duck-typed transports, and torn snapshot copies were rejected.

**Consequences:** broader but explicit profile/gateway/test changes; no live action or deployment automation; strong constructor, lock, restart, and terminal-state invariants.

**Follow-ups:** human reuses/provisions test Telegram destinations, performs live P2–P6, and approves any deployment.
