gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-1890-
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py:1891:def require_task22_supplemental_for_activation(
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-1892-    profile_root: Path | str,
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-1893-    *,
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-1894-    customer_key: str,
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-1895-    session_id: str,
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-1896-) -> bool:
--
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-2340-    "SupplementalReviewPins",
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-2341-    "SupplementalReviewSnapshot",
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-2342-    "SupplementalReviewStore",
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-2343-    "discover_supplemental_review_store",
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-2344-    "require_supplemental_owner_approval",
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py:2345:    "require_task22_supplemental_for_activation",
gateway/platforms/telegram_nutrition_onboarding_supplemental_review.py-2346-]
--
gateway/platforms/rehearsal_reset.py-1682-    *,
gateway/platforms/rehearsal_reset.py-1683-    closure: str,
gateway/platforms/rehearsal_reset.py-1684-    archived: bool = False,
gateway/platforms/rehearsal_reset.py-1685-) -> None:
gateway/platforms/rehearsal_reset.py-1686-    migrations = (
gateway/platforms/rehearsal_reset.py:1687:        root / "data" / "customers" / "task22_dm_rehearsal"
gateway/platforms/rehearsal_reset.py-1688-        / "nutrition-onboarding" / "migrations"
gateway/platforms/rehearsal_reset.py-1689-    )
gateway/platforms/rehearsal_reset.py-1690-    receipt = migrations / "task22-callback-storage-adoption-v1.json"
gateway/platforms/rehearsal_reset.py-1691-    outbox = root / "data" / "onboarding" / "telegram-publication-outbox-v1"
gateway/platforms/rehearsal_reset.py-1692-    ledger = outbox / "owner-callbacks.json"
--
gateway/platforms/rehearsal_reset.py-1810-    receipt_required = _trainer_removal_receipt_required(
gateway/platforms/rehearsal_reset.py-1811-        payload / "customers" / "registry.json",
gateway/platforms/rehearsal_reset.py-1812-        closure="archive",
gateway/platforms/rehearsal_reset.py-1813-    )
gateway/platforms/rehearsal_reset.py-1814-    transaction = (
gateway/platforms/rehearsal_reset.py:1815:        payload / "data" / "customers" / "task22_dm_rehearsal"
gateway/platforms/rehearsal_reset.py-1816-        / "nutrition-onboarding" / "migrations" / "trainer-authority-removal-v1"
gateway/platforms/rehearsal_reset.py-1817-    )
gateway/platforms/rehearsal_reset.py-1818-    lock = transaction.parent / ".trainer-authority-removal-v1.lock"
gateway/platforms/rehearsal_reset.py-1819-    if not transaction.exists() and not transaction.is_symlink() and not lock.exists() and not lock.is_symlink():
gateway/platforms/rehearsal_reset.py-1820-        if not receipt_required:
--
gateway/platforms/rehearsal_reset.py-3006-    receipt_required = _trainer_removal_receipt_required(
gateway/platforms/rehearsal_reset.py-3007-        root / "customers" / "registry.json",
gateway/platforms/rehearsal_reset.py-3008-        closure="reset",
gateway/platforms/rehearsal_reset.py-3009-    )
gateway/platforms/rehearsal_reset.py-3010-    migration_root = (
gateway/platforms/rehearsal_reset.py:3011:        root / "data" / "customers" / "task22_dm_rehearsal"
gateway/platforms/rehearsal_reset.py-3012-        / "nutrition-onboarding" / "migrations" / "trainer-authority-removal-v1"
gateway/platforms/rehearsal_reset.py-3013-    )
gateway/platforms/rehearsal_reset.py-3014-    lock = migration_root.parent / ".trainer-authority-removal-v1.lock"
gateway/platforms/rehearsal_reset.py-3015-    if not migration_root.exists() and not migration_root.is_symlink() and not lock.exists() and not lock.is_symlink():
gateway/platforms/rehearsal_reset.py-3016-        if not receipt_required:
--
scripts/task22-supplemental-owner-review-18-from typing import Any, Iterator, cast
scripts/task22-supplemental-owner-review-19-
scripts/task22-supplemental-owner-review-20-sys.dont_write_bytecode = True
scripts/task22-supplemental-owner-review-21-
scripts/task22-supplemental-owner-review-22-_PROFILE = "dualcoachtest"
scripts/task22-supplemental-owner-review:23:_CUSTOMER = "task22_dm_rehearsal"
scripts/task22-supplemental-owner-review-24-_SESSION = "rb_Ar-NuKG3LH8CqHPXUgc88g"
scripts/task22-supplemental-owner-review-25-_PROFILE_ROOT = Path("/home/cube/.hermes/profiles/dualcoachtest")
scripts/task22-supplemental-owner-review-26-_MANIFEST = Path(
scripts/task22-supplemental-owner-review-27-    "/home/cube/projects/richard/traning coach/.omo/evidence/dualcoach-task22-supplemental-successor-manifest.json"
scripts/task22-supplemental-owner-review-28-)
--
gateway/platforms/telegram_room_bootstrap_cutover.py-31-    require_activation_readiness,
gateway/platforms/telegram_room_bootstrap_cutover.py-32-    validate_activation_readiness_receipt,
gateway/platforms/telegram_room_bootstrap_cutover.py-33-)
gateway/platforms/telegram_room_bootstrap_cutover.py-34-from gateway.platforms.telegram_nutrition_onboarding_supplemental_review import (
gateway/platforms/telegram_room_bootstrap_cutover.py-35-    SupplementalReviewError,
gateway/platforms/telegram_room_bootstrap_cutover.py:36:    require_task22_supplemental_for_activation,
gateway/platforms/telegram_room_bootstrap_cutover.py-37-)
gateway/platforms/telegram_room_bootstrap_cutover.py-38-from gateway.platforms.telegram_nutrition_addresses import (
gateway/platforms/telegram_room_bootstrap_cutover.py-39-    AddressConfigurationError,
gateway/platforms/telegram_room_bootstrap_cutover.py-40-    parse_customer_private_delivery_address,
gateway/platforms/telegram_room_bootstrap_cutover.py-41-)
--
gateway/platforms/telegram_room_bootstrap_cutover.py-461-        if (
gateway/platforms/telegram_room_bootstrap_cutover.py-462-            live.state is BootstrapState.FAILED
gateway/platforms/telegram_room_bootstrap_cutover.py-463-            and live.failure_code == "activation_authority_rejected"
gateway/platforms/telegram_room_bootstrap_cutover.py-464-        ):
gateway/platforms/telegram_room_bootstrap_cutover.py-465-            raise ActivationCutoverError(
gateway/platforms/telegram_room_bootstrap_cutover.py:466:                "terminal activation recovery requires a new supported bootstrap"
gateway/platforms/telegram_room_bootstrap_cutover.py-467-            )
gateway/platforms/telegram_room_bootstrap_cutover.py-468-        if live.state not in {BootstrapState.AWAITING_ACTIVATION, BootstrapState.ACTIVE}:
gateway/platforms/telegram_room_bootstrap_cutover.py-469-            raise ActivationCutoverError("session is not AWAITING_ACTIVATION or completed")
gateway/platforms/telegram_room_bootstrap_cutover.py-470-
gateway/platforms/telegram_room_bootstrap_cutover.py-471-        registry_path = admin._resolve_registry_path(self.profile_root)
--
gateway/platforms/telegram_room_bootstrap_cutover.py-524-                recovery_document,
gateway/platforms/telegram_room_bootstrap_cutover.py-525-                recovery_current,
gateway/platforms/telegram_room_bootstrap_cutover.py-526-            )
gateway/platforms/telegram_room_bootstrap_cutover.py-527-
gateway/platforms/telegram_room_bootstrap_cutover.py-528-        try:
gateway/platforms/telegram_room_bootstrap_cutover.py:529:            require_task22_supplemental_for_activation(
gateway/platforms/telegram_room_bootstrap_cutover.py-530-                self.profile_root,
gateway/platforms/telegram_room_bootstrap_cutover.py-531-                customer_key=live.customer_key,
gateway/platforms/telegram_room_bootstrap_cutover.py-532-                session_id=live.session_id,
gateway/platforms/telegram_room_bootstrap_cutover.py-533-            )
gateway/platforms/telegram_room_bootstrap_cutover.py-534-            if phase == "ready":
--
gateway/platforms/task22_callback_storage_adoption.py-31-_TARGET_UNIT: Final = "hermes-gateway-dualcoachtest.service"
gateway/platforms/task22_callback_storage_adoption.py-32-_USER_SYSTEMD_CMDLINE: Final = b"/usr/lib/systemd/systemd\0--user\0"
gateway/platforms/task22_callback_storage_adoption.py-33-_SSH_AGENT_EXECUTABLE: Final = Path("/usr/bin/ssh-agent")
gateway/platforms/task22_callback_storage_adoption.py-34-_PTRACE_SCOPE: Final = Path("/proc/sys/kernel/yama/ptrace_scope")
gateway/platforms/task22_callback_storage_adoption.py-35-_BOOT_ID: Final = Path("/proc/sys/kernel/random/boot_id")
gateway/platforms/task22_callback_storage_adoption.py:36:CUSTOMER: Final = "task22_dm_rehearsal"
gateway/platforms/task22_callback_storage_adoption.py-37-SESSION: Final = "rb_Ar-NuKG3LH8CqHPXUgc88g"
gateway/platforms/task22_callback_storage_adoption.py-38-CANDIDATE_DIGEST: Final = "a6dc172050add2d352ebfb7efc7615c837ef2365656b8ca6f361255cb3b0157a"
gateway/platforms/task22_callback_storage_adoption.py-39-MANIFEST_DIGEST: Final = "c5f198be3e04a5df8d49d827a22b43eaf6fcc3791334d983edc5ad1abf30a72a"
gateway/platforms/task22_callback_storage_adoption.py-40-FREEZE_DIGEST: Final = "a85689de997916ff05b63531f137bc30c1c5ef72ef61b8587452d5d39e420bcc"
gateway/platforms/task22_callback_storage_adoption.py-41-HISTORICAL_ABSENT_DIGEST: Final = "359adbba7bca9cfebfdeb049bc8a06c970e415d3250178910e187b745ca5987d"
--
tests/gateway/test_task22_supplemental_owner_review.py-31-    SupplementalReviewPaths,
tests/gateway/test_task22_supplemental_owner_review.py-32-    SupplementalReviewPhase,
tests/gateway/test_task22_supplemental_owner_review.py-33-    SupplementalReviewPins,
tests/gateway/test_task22_supplemental_owner_review.py-34-    SupplementalReviewStore,
tests/gateway/test_task22_supplemental_owner_review.py-35-    discover_supplemental_review_store,
tests/gateway/test_task22_supplemental_owner_review.py:36:    require_task22_supplemental_for_activation,
tests/gateway/test_task22_supplemental_owner_review.py-37-)
tests/gateway/test_task22_supplemental_owner_review.py-38-
tests/gateway/test_task22_supplemental_owner_review.py-39-
tests/gateway/test_task22_supplemental_owner_review.py-40-def _write(path: Path, value: object) -> None:
tests/gateway/test_task22_supplemental_owner_review.py-41-    path.parent.mkdir(parents=True, exist_ok=True)
--
tests/gateway/test_task22_supplemental_owner_review.py-875-    completed = store.commit_publication(authority=authority, route=route)
tests/gateway/test_task22_supplemental_owner_review.py-876-    assert completed.phase is SupplementalReviewPhase.PUBLICATION_COMMITTED
tests/gateway/test_task22_supplemental_owner_review.py-877-    assert store.commit_publication(authority=authority, route=route) == completed
tests/gateway/test_task22_supplemental_owner_review.py-878-    assert all(path.read_bytes() == data for path, data in protected.items())
tests/gateway/test_task22_supplemental_owner_review.py-879-    assert [item.generation for item in store.outbox.records()] == [23, 25, 26, 27, 28]
tests/gateway/test_task22_supplemental_owner_review.py:880:    assert require_task22_supplemental_for_activation(
tests/gateway/test_task22_supplemental_owner_review.py-881-        store.paths.profile_root,
tests/gateway/test_task22_supplemental_owner_review.py-882-        customer_key="customer",
tests/gateway/test_task22_supplemental_owner_review.py-883-        session_id="sid",
tests/gateway/test_task22_supplemental_owner_review.py-884-    ) is True
tests/gateway/test_task22_supplemental_owner_review.py-885-
--
tests/gateway/test_task22_supplemental_owner_review.py-943-def test_task22_preflight_requires_and_seals_committed_trainer_removal_receipt(
tests/gateway/test_task22_supplemental_owner_review.py-944-    tmp_path: Path,
tests/gateway/test_task22_supplemental_owner_review.py-945-    monkeypatch: pytest.MonkeyPatch,
tests/gateway/test_task22_supplemental_owner_review.py-946-) -> None:
tests/gateway/test_task22_supplemental_owner_review.py-947-    store, authority, route = _fixture(tmp_path)
tests/gateway/test_task22_supplemental_owner_review.py:948:    store.pins = replace(store.pins, customer_key="task22_dm_rehearsal")
tests/gateway/test_task22_supplemental_owner_review.py-949-    registry = json.loads(store.paths.registry.read_text())
tests/gateway/test_task22_supplemental_owner_review.py:950:    registry["customers"][0]["customer_key"] = "task22_dm_rehearsal"
tests/gateway/test_task22_supplemental_owner_review.py-951-    _write(store.paths.registry, registry)
tests/gateway/test_task22_supplemental_owner_review.py-952-    bootstrap = json.loads(store.paths.bootstrap.read_text())
tests/gateway/test_task22_supplemental_owner_review.py:953:    bootstrap["sessions"][0]["customer_key"] = "task22_dm_rehearsal"
tests/gateway/test_task22_supplemental_owner_review.py-954-    _write(store.paths.bootstrap, bootstrap)
tests/gateway/test_task22_supplemental_owner_review.py-955-    from gateway.platforms.task22_trainer_authority_removal import RemovalPaths
tests/gateway/test_task22_supplemental_owner_review.py-956-    import gateway.platforms.task22_callback_storage_adoption as adoption
tests/gateway/test_task22_supplemental_owner_review.py-957-    import gateway.platforms.task22_trainer_authority_removal as removal
tests/gateway/test_task22_supplemental_owner_review.py-958-
--
tests/gateway/test_task22_supplemental_owner_review.py-1121-    }
tests/gateway/test_task22_supplemental_owner_review.py-1122-    _write(historical_manifest, historical_manifest_data)
tests/gateway/test_task22_supplemental_owner_review.py-1123-    history_paths = SupplementalReviewPaths.canonical(
tests/gateway/test_task22_supplemental_owner_review.py-1124-        store.paths.profile_root,
tests/gateway/test_task22_supplemental_owner_review.py-1125-        candidate_manifest=historical_manifest,
tests/gateway/test_task22_supplemental_owner_review.py:1126:        customer_key="task22_dm_rehearsal",
tests/gateway/test_task22_supplemental_owner_review.py-1127-    )
tests/gateway/test_task22_supplemental_owner_review.py-1128-    for source, target in (
tests/gateway/test_task22_supplemental_owner_review.py-1129-        (store.paths.baseline_candidate, history_paths.baseline_candidate),
tests/gateway/test_task22_supplemental_owner_review.py-1130-        (store.paths.baseline, history_paths.baseline),
tests/gateway/test_task22_supplemental_owner_review.py-1131-        (store.paths.ready, history_paths.ready),
--
tests/gateway/test_task22_supplemental_owner_review.py-1142-        history_paths.onboarding_root.parent.parent,
tests/gateway/test_task22_supplemental_owner_review.py-1143-        history_paths.journal.parent,
tests/gateway/test_task22_supplemental_owner_review.py-1144-    ):
tests/gateway/test_task22_supplemental_owner_review.py-1145-        directory.chmod(0o700)
tests/gateway/test_task22_supplemental_owner_review.py-1146-    registry = json.loads(history_paths.registry.read_text())
tests/gateway/test_task22_supplemental_owner_review.py:1147:    registry["customers"][0]["customer_key"] = "task22_dm_rehearsal"
tests/gateway/test_task22_supplemental_owner_review.py-1148-    _write(history_paths.registry, registry)
tests/gateway/test_task22_supplemental_owner_review.py-1149-    bootstrap = json.loads(history_paths.bootstrap.read_text())
tests/gateway/test_task22_supplemental_owner_review.py:1150:    bootstrap["sessions"][0]["customer_key"] = "task22_dm_rehearsal"
tests/gateway/test_task22_supplemental_owner_review.py-1151-    _write(history_paths.bootstrap, bootstrap)
tests/gateway/test_task22_supplemental_owner_review.py-1152-    receipt_paths = RemovalPaths.canonical(
tests/gateway/test_task22_supplemental_owner_review.py-1153-        history_paths.profile_root,
tests/gateway/test_task22_supplemental_owner_review.py-1154-        candidate_manifest=historical_manifest,
tests/gateway/test_task22_supplemental_owner_review.py-1155-        freeze_receipt=historical_manifest,
--
tests/gateway/test_task22_supplemental_owner_review.py-1189-    )
tests/gateway/test_task22_supplemental_owner_review.py-1190-    historical_store = SupplementalReviewStore(
tests/gateway/test_task22_supplemental_owner_review.py-1191-        history_paths,
tests/gateway/test_task22_supplemental_owner_review.py-1192-        SupplementalReviewPins(
tests/gateway/test_task22_supplemental_owner_review.py-1193-            profile="dualcoachtest",
tests/gateway/test_task22_supplemental_owner_review.py:1194:            customer_key="task22_dm_rehearsal",
tests/gateway/test_task22_supplemental_owner_review.py-1195-            session_id="sid",
tests/gateway/test_task22_supplemental_owner_review.py-1196-            original_transaction_id="original-tx",
tests/gateway/test_task22_supplemental_owner_review.py-1197-            candidate_digest=historical_candidate,
tests/gateway/test_task22_supplemental_owner_review.py-1198-        ),
tests/gateway/test_task22_supplemental_owner_review.py-1199-        outbox=store.outbox,
--
tests/gateway/test_task22_supplemental_owner_review.py-1262-            "live_profile_accessed": False,
tests/gateway/test_task22_supplemental_owner_review.py-1263-        },
tests/gateway/test_task22_supplemental_owner_review.py-1264-    }
tests/gateway/test_task22_supplemental_owner_review.py-1265-    successor_path = evidence_root / "dualcoach-task22-supplemental-successor-manifest.json"
tests/gateway/test_task22_supplemental_owner_review.py-1266-    _write(successor_path, successor)
tests/gateway/test_task22_supplemental_owner_review.py:1267:    monkeypatch.setattr(supplemental, "_TASK22_SESSION", "sid")
tests/gateway/test_task22_supplemental_owner_review.py-1268-    monkeypatch.setattr(supplemental, "_TASK22_HISTORICAL_CANDIDATE", historical_candidate)
tests/gateway/test_task22_supplemental_owner_review.py-1269-    monkeypatch.setattr(
tests/gateway/test_task22_supplemental_owner_review.py-1270-        supplemental,
tests/gateway/test_task22_supplemental_owner_review.py-1271-        "_TASK22_HISTORICAL_MANIFEST_SHA256",
tests/gateway/test_task22_supplemental_owner_review.py-1272-        hashlib.sha256(historical_manifest.read_bytes()).hexdigest(),
--
tests/gateway/test_task22_supplemental_owner_review.py-1297-    monkeypatch.setattr(supplemental, "_TASK22_SUCCESSOR_BINDING", successor)
tests/gateway/test_task22_supplemental_owner_review.py-1298-    successor_store = SupplementalReviewStore(
tests/gateway/test_task22_supplemental_owner_review.py-1299-        SupplementalReviewPaths.canonical(
tests/gateway/test_task22_supplemental_owner_review.py-1300-            history_paths.profile_root,
tests/gateway/test_task22_supplemental_owner_review.py-1301-            candidate_manifest=successor_path,
tests/gateway/test_task22_supplemental_owner_review.py:1302:            customer_key="task22_dm_rehearsal",
tests/gateway/test_task22_supplemental_owner_review.py-1303-        ),
tests/gateway/test_task22_supplemental_owner_review.py-1304-        SupplementalReviewPins(
tests/gateway/test_task22_supplemental_owner_review.py-1305-            profile="dualcoachtest",
tests/gateway/test_task22_supplemental_owner_review.py:1306:            customer_key="task22_dm_rehearsal",
tests/gateway/test_task22_supplemental_owner_review.py-1307-            session_id="sid",
tests/gateway/test_task22_supplemental_owner_review.py-1308-            original_transaction_id="original-tx",
tests/gateway/test_task22_supplemental_owner_review.py-1309-            candidate_digest=successor_candidate,
tests/gateway/test_task22_supplemental_owner_review.py-1310-        ),
tests/gateway/test_task22_supplemental_owner_review.py-1311-        outbox=historical_store.outbox,
--
tests/gateway/test_task22_supplemental_owner_review.py-1345-    store, authority, route = _fixture(tmp_path)
tests/gateway/test_task22_supplemental_owner_review.py-1346-    store.prepare(authority=authority, route=route)
tests/gateway/test_task22_supplemental_owner_review.py-1347-    if phase == "review":
tests/gateway/test_task22_supplemental_owner_review.py-1348-        _prepare_and_review(store, authority, route)
tests/gateway/test_task22_supplemental_owner_review.py-1349-    with pytest.raises(SupplementalReviewError, match="approval"):
tests/gateway/test_task22_supplemental_owner_review.py:1350:        require_task22_supplemental_for_activation(
tests/gateway/test_task22_supplemental_owner_review.py-1351-            store.paths.profile_root,
tests/gateway/test_task22_supplemental_owner_review.py-1352-            customer_key="customer",
tests/gateway/test_task22_supplemental_owner_review.py-1353-            session_id="sid",
tests/gateway/test_task22_supplemental_owner_review.py-1354-        )
tests/gateway/test_task22_supplemental_owner_review.py-1355-
--
tests/gateway/test_task22_supplemental_owner_review.py-1663-        "version": 3,
tests/gateway/test_task22_supplemental_owner_review.py-1664-    })
tests/gateway/test_task22_supplemental_owner_review.py-1665-    answers = json.loads(store.paths.baseline_candidate.read_text())["source_answers"]
tests/gateway/test_task22_supplemental_owner_review.py-1666-    monkeypatch.setattr(supplemental, "_TASK22_PROFILE", "dualcoachtest")
tests/gateway/test_task22_supplemental_owner_review.py-1667-    monkeypatch.setattr(supplemental, "_TASK22_CUSTOMER", "customer")
tests/gateway/test_task22_supplemental_owner_review.py:1668:    monkeypatch.setattr(supplemental, "_TASK22_SESSION", "sid")
tests/gateway/test_task22_supplemental_owner_review.py-1669-    monkeypatch.setattr(supplemental, "_TASK22_CALLBACK_EVENT_INTEGRITY", event.event_integrity)
tests/gateway/test_task22_supplemental_owner_review.py-1670-    monkeypatch.setattr(supplemental, "_TASK22_CALLBACK_DATA", callback_data)
tests/gateway/test_task22_supplemental_owner_review.py-1671-    monkeypatch.setattr(supplemental, "_TASK22_EMBEDDED_SID_HASH", embedded_hash)
tests/gateway/test_task22_supplemental_owner_review.py:1672:    monkeypatch.setattr(supplemental, "_TASK22_SESSION_SHA256", hashlib.sha256(b"sid").hexdigest())
tests/gateway/test_task22_supplemental_owner_review.py-1673-    monkeypatch.setattr(supplemental, "_TASK22_ANSWERS_DIGEST", hashlib.sha256(
tests/gateway/test_task22_supplemental_owner_review.py-1674-        json.dumps(answers, sort_keys=True, separators=(",", ":")).encode()
tests/gateway/test_task22_supplemental_owner_review.py-1675-    ).hexdigest())
tests/gateway/test_task22_supplemental_owner_review.py-1676-    monkeypatch.setattr(supplemental, "_TASK22_INGRESS_RECEIPT_NAME", ingress.name)
tests/gateway/test_task22_supplemental_owner_review.py-1677-    monkeypatch.setattr(supplemental, "_TASK22_CALLBACK_UPDATE_ID", 500)
--
tests/gateway/test_task22_trainer_authority_removal.py-143-
tests/gateway/test_task22_trainer_authority_removal.py-144-
tests/gateway/test_task22_trainer_authority_removal.py-145-def _write(path: Path, value: object) -> bytes:
tests/gateway/test_task22_trainer_authority_removal.py-146-    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
tests/gateway/test_task22_trainer_authority_removal.py-147-    for parent in (path.parent, *path.parent.parents):
tests/gateway/test_task22_trainer_authority_removal.py:148:        if parent.name in {"dualcoachtest", "data", "customers", "task22_dm_rehearsal", "nutrition-onboarding", "migrations", "evidence", "onboarding", "telegram-publication-outbox-v1", "owner-actions"}:
tests/gateway/test_task22_trainer_authority_removal.py-149-            parent.chmod(0o700)
tests/gateway/test_task22_trainer_authority_removal.py-150-    data = value if isinstance(value, bytes) else (
tests/gateway/test_task22_trainer_authority_removal.py-151-        json.dumps(value, ensure_ascii=False, indent=2) + "\n"
tests/gateway/test_task22_trainer_authority_removal.py-152-    ).encode()
tests/gateway/test_task22_trainer_authority_removal.py-153-    path.write_bytes(data)
--
tests/gateway/test_task22_trainer_authority_removal.py-320-    assert calls == ["preflight", "execute"]
tests/gateway/test_task22_trainer_authority_removal.py-321-
tests/gateway/test_task22_trainer_authority_removal.py-322-
tests/gateway/test_task22_trainer_authority_removal.py-323-def test_production_pins_and_exact_semantic_diff_are_immutable() -> None:
tests/gateway/test_task22_trainer_authority_removal.py-324-    assert removal.PROFILE == "dualcoachtest"
tests/gateway/test_task22_trainer_authority_removal.py:325:    assert removal.CUSTOMER == "task22_dm_rehearsal"
tests/gateway/test_task22_trainer_authority_removal.py-326-    assert removal.SESSION == "rb_Ar-NuKG3LH8CqHPXUgc88g"
tests/gateway/test_task22_trainer_authority_removal.py-327-    assert removal.SOURCE_DIGEST == "b5b95afa818b0c7460141403e0bfe434fb987a31fe5285f6e6e98b70cb738aa1"
tests/gateway/test_task22_trainer_authority_removal.py-328-    assert removal.TARGET_DIGEST == "69c1382eb663942a7c8c18b9fee29141612ac37e92f1ffb7df0ba1beed44579d"
tests/gateway/test_task22_trainer_authority_removal.py-329-    assert removal.HISTORICAL_REGISTRY_DIGEST == "93951fae167d8a978ec9edcc2e31ce3cbd4de95442579f293cda2f3715269fa2"
tests/gateway/test_task22_trainer_authority_removal.py:330:    assert removal.SEMANTIC_DIFF == ({"op": "remove", "path": "/customers/task22_dm_rehearsal/trainer"},)
tests/gateway/test_task22_trainer_authority_removal.py-331-
tests/gateway/test_task22_trainer_authority_removal.py-332-
tests/gateway/test_task22_trainer_authority_removal.py-333-def test_trainer_removal_requires_adoption_first(
tests/gateway/test_task22_trainer_authority_removal.py-334-    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
tests/gateway/test_task22_trainer_authority_removal.py-335-) -> None:
Expire implementation:
gateway/platforms/telegram_room_bootstrap.py-88-    PREPARED = "PREPARED"
gateway/platforms/telegram_room_bootstrap.py-89-    WAITING_ADMIN = "WAITING_ADMIN"
gateway/platforms/telegram_room_bootstrap.py-90-    PROVISIONING = "PROVISIONING"
gateway/platforms/telegram_room_bootstrap.py-91-    CLAIMING = "CLAIMING"
gateway/platforms/telegram_room_bootstrap.py-92-    ROLE_REVIEW = "ROLE_REVIEW"
gateway/platforms/telegram_room_bootstrap.py-93-    REGISTERING = "REGISTERING"
gateway/platforms/telegram_room_bootstrap.py-94-    AWAITING_CONSENT = "AWAITING_CONSENT"
gateway/platforms/telegram_room_bootstrap.py-95-    AWAITING_ACTIVATION = "AWAITING_ACTIVATION"
gateway/platforms/telegram_room_bootstrap.py-96-    ACTIVE = "ACTIVE"
gateway/platforms/telegram_room_bootstrap.py-97-    CANCELLED = "CANCELLED"
gateway/platforms/telegram_room_bootstrap.py:98:    EXPIRED = "EXPIRED"
gateway/platforms/telegram_room_bootstrap.py-99-    FAILED = "FAILED"
gateway/platforms/telegram_room_bootstrap.py-100-
gateway/platforms/telegram_room_bootstrap.py-101-
gateway/platforms/telegram_room_bootstrap.py-102-class Role(str, Enum):
gateway/platforms/telegram_room_bootstrap.py-103-    CUSTOMER = "customer"
gateway/platforms/telegram_room_bootstrap.py-104-    TRAINER = "trainer"
gateway/platforms/telegram_room_bootstrap.py-105-
gateway/platforms/telegram_room_bootstrap.py-106-
gateway/platforms/telegram_room_bootstrap.py-107-class TopicSlot(str, Enum):
gateway/platforms/telegram_room_bootstrap.py-108-    CHECKIN = "checkin"
gateway/platforms/telegram_room_bootstrap.py-109-    COACHING = "coaching"
gateway/platforms/telegram_room_bootstrap.py-110-    TRAINER_REVIEW = "trainer_review"
gateway/platforms/telegram_room_bootstrap.py-111-    ADMIN = "admin"
gateway/platforms/telegram_room_bootstrap.py-112-
gateway/platforms/telegram_room_bootstrap.py-113-
gateway/platforms/telegram_room_bootstrap.py-114-class RecoverySlot(str, Enum):
gateway/platforms/telegram_room_bootstrap.py-115-    """One externally visible bootstrap side effect requiring a receipt."""
gateway/platforms/telegram_room_bootstrap.py-116-
gateway/platforms/telegram_room_bootstrap.py-117-    TOPIC_CUSTOMER = "topic_customer"
gateway/platforms/telegram_room_bootstrap.py-118-    TOPIC_TRAINER = "topic_trainer"
gateway/platforms/telegram_room_bootstrap.py-119-    CLAIM_CUSTOMER = "claim_customer"
gateway/platforms/telegram_room_bootstrap.py-120-    CLAIM_TRAINER = "claim_trainer"
gateway/platforms/telegram_room_bootstrap.py-121-    OWNER_REVIEW = "owner_review"
gateway/platforms/telegram_room_bootstrap.py-122-    CONSENT_CARD = "consent_card"
gateway/platforms/telegram_room_bootstrap.py-123-
gateway/platforms/telegram_room_bootstrap.py-124-
gateway/platforms/telegram_room_bootstrap.py-125-_TERMINAL: Final = frozenset(
gateway/platforms/telegram_room_bootstrap.py:126:    {BootstrapState.ACTIVE, BootstrapState.CANCELLED, BootstrapState.EXPIRED, BootstrapState.FAILED}
gateway/platforms/telegram_room_bootstrap.py-127-)
gateway/platforms/telegram_room_bootstrap.py-128-_TRANSITIONS: Final[Mapping[BootstrapState, frozenset[BootstrapState]]] = {
gateway/platforms/telegram_room_bootstrap.py-129-    BootstrapState.PREPARED: frozenset({
gateway/platforms/telegram_room_bootstrap.py-130-        BootstrapState.WAITING_ADMIN,
gateway/platforms/telegram_room_bootstrap.py-131-        BootstrapState.PROVISIONING,
gateway/platforms/telegram_room_bootstrap.py-132-        BootstrapState.REGISTERING,
gateway/platforms/telegram_room_bootstrap.py-133-    }),
gateway/platforms/telegram_room_bootstrap.py-134-    BootstrapState.WAITING_ADMIN: frozenset({BootstrapState.PROVISIONING}),
gateway/platforms/telegram_room_bootstrap.py-135-    BootstrapState.PROVISIONING: frozenset({BootstrapState.CLAIMING}),
gateway/platforms/telegram_room_bootstrap.py-136-    BootstrapState.CLAIMING: frozenset({BootstrapState.ROLE_REVIEW}),
gateway/platforms/telegram_room_bootstrap.py-137-    BootstrapState.ROLE_REVIEW: frozenset({BootstrapState.REGISTERING}),
gateway/platforms/telegram_room_bootstrap.py-138-    BootstrapState.REGISTERING: frozenset({BootstrapState.AWAITING_CONSENT}),
gateway/platforms/telegram_room_bootstrap.py-139-    BootstrapState.AWAITING_CONSENT: frozenset({BootstrapState.AWAITING_ACTIVATION}),
gateway/platforms/telegram_room_bootstrap.py-140-}
gateway/platforms/telegram_room_bootstrap.py-141-
gateway/platforms/telegram_room_bootstrap.py-142-
gateway/platforms/telegram_room_bootstrap.py-143-@dataclass(frozen=True, slots=True)
gateway/platforms/telegram_room_bootstrap.py-144-class MessageRoute:
gateway/platforms/telegram_room_bootstrap.py-145-    user_id: str
gateway/platforms/telegram_room_bootstrap.py-146-    chat_id: str
gateway/platforms/telegram_room_bootstrap.py-147-    topic_id: str | None
gateway/platforms/telegram_room_bootstrap.py-148-    message_id: str
gateway/platforms/telegram_room_bootstrap.py-149-
gateway/platforms/telegram_room_bootstrap.py-150-
gateway/platforms/telegram_room_bootstrap.py-151-@dataclass(frozen=True, slots=True)
gateway/platforms/telegram_room_bootstrap.py-152-class RoleClaim:
gateway/platforms/telegram_room_bootstrap.py-153-    role: Role
gateway/platforms/telegram_room_bootstrap.py-154-    user_id: str
gateway/platforms/telegram_room_bootstrap.py-155-    chat_id: str
gateway/platforms/telegram_room_bootstrap.py-156-    topic_id: str
gateway/platforms/telegram_room_bootstrap.py-157-    message_id: str
gateway/platforms/telegram_room_bootstrap.py-158-
gateway/platforms/telegram_room_bootstrap.py-159-
gateway/platforms/telegram_room_bootstrap.py-160-@dataclass(frozen=True, slots=True)
gateway/platforms/telegram_room_bootstrap.py-161-class ClaimCard:
gateway/platforms/telegram_room_bootstrap.py-162-    role: Role
gateway/platforms/telegram_room_bootstrap.py-163-    message_id: str
gateway/platforms/telegram_room_bootstrap.py-164-
gateway/platforms/telegram_room_bootstrap.py-165-
gateway/platforms/telegram_room_bootstrap.py-166-@dataclass(frozen=True, slots=True)
gateway/platforms/telegram_room_bootstrap.py-167-class RecoveryAttempt:
gateway/platforms/telegram_room_bootstrap.py-168-    session_id: str
gateway/platforms/telegram_room_bootstrap.py-169-    slot: RecoverySlot
gateway/platforms/telegram_room_bootstrap.py-170-    chat_id: str
gateway/platforms/telegram_room_bootstrap.py-171-    generation: int
gateway/platforms/telegram_room_bootstrap.py-172-    created_at: datetime
gateway/platforms/telegram_room_bootstrap.py-173-
gateway/platforms/telegram_room_bootstrap.py-174-
gateway/platforms/telegram_room_bootstrap.py-175-@dataclass(frozen=True, slots=True)
gateway/platforms/telegram_room_bootstrap.py-176-class ParsedCallback:
gateway/platforms/telegram_room_bootstrap.py-177-    action: str
gateway/platforms/telegram_room_bootstrap.py-178-    sid_hash: str
gateway/platforms/telegram_room_bootstrap.py-179-    generation: int
gateway/platforms/telegram_room_bootstrap.py-180-
gateway/platforms/telegram_room_bootstrap.py-181-
gateway/platforms/telegram_room_bootstrap.py-182-@dataclass(frozen=True, slots=True)
gateway/platforms/telegram_room_bootstrap.py-183-class CustomerDraft:
gateway/platforms/telegram_room_bootstrap.py-184-    customer_key: str
gateway/platforms/telegram_room_bootstrap.py-185-    display_name: str
gateway/platforms/telegram_room_bootstrap.py-186-    starts_on: str
gateway/platforms/telegram_room_bootstrap.py-187-    daily_time: str
gateway/platforms/telegram_room_bootstrap.py-188-    weekly_weekday: int
gateway/platforms/telegram_room_bootstrap.py-189-    monthly_day: int
gateway/platforms/telegram_room_bootstrap.py-190-    calories_kcal: int
gateway/platforms/telegram_room_bootstrap.py-191-    protein_g: int
gateway/platforms/telegram_room_bootstrap.py-192-    meals: tuple[str, ...]
gateway/platforms/telegram_room_bootstrap.py-193-    primary_goal: str = "미정"
gateway/platforms/telegram_room_bootstrap.py-194-    dietary_restrictions: tuple[str, ...] = ()
gateway/platforms/telegram_room_bootstrap.py-195-    allergies: tuple[str, ...] = ()
gateway/platforms/telegram_room_bootstrap.py-196-    food_preferences: tuple[str, ...] = ()
gateway/platforms/telegram_room_bootstrap.py-197-    supplements: tuple[str, ...] = ()
gateway/platforms/telegram_room_bootstrap.py-198-    digestion_context: str | None = None
gateway/platforms/telegram_room_bootstrap.py-199-    sleep_goal_hours: float | None = None
gateway/platforms/telegram_room_bootstrap.py-200-    recovery_goal: str | None = None
gateway/platforms/telegram_room_bootstrap.py-201-    training_context: str | None = None
gateway/platforms/telegram_room_bootstrap.py-202-    carbohydrate_g: int | None = None
gateway/platforms/telegram_room_bootstrap.py-203-    fat_g: int | None = None
gateway/platforms/telegram_room_bootstrap.py-204-    water_liters: float | None = None
gateway/platforms/telegram_room_bootstrap.py-205-    customer_user_id: str | None = None
gateway/platforms/telegram_room_bootstrap.py-206-    trainer_user_id: str | None = None
gateway/platforms/telegram_room_bootstrap.py-207-    trainer_chat_id: str | None = None
gateway/platforms/telegram_room_bootstrap.py-208-    trainer_topic_id: str | None = None
gateway/platforms/telegram_room_bootstrap.py-209-
gateway/platforms/telegram_room_bootstrap.py-210-    @property
gateway/platforms/telegram_room_bootstrap.py-211-    def canonical_json(self) -> str:
gateway/platforms/telegram_room_bootstrap.py-212-        return _canonical_json(self.to_dict())
gateway/platforms/telegram_room_bootstrap.py-213-
gateway/platforms/telegram_room_bootstrap.py-214-    @property
gateway/platforms/telegram_room_bootstrap.py-215-    def digest(self) -> str:
gateway/platforms/telegram_room_bootstrap.py-216-        return _sha256(self.canonical_json.encode("utf-8"))
--
gateway/platforms/telegram_room_bootstrap.py-1487-            claims = {claim.role: claim for claim in session.role_claims}
gateway/platforms/telegram_room_bootstrap.py-1488-            if role in claims:
gateway/platforms/telegram_room_bootstrap.py-1489-                raise GenerationConflict("role claim capability is stale")
gateway/platforms/telegram_room_bootstrap.py-1490-            if any(claim.user_id == user for claim in claims.values()):
gateway/platforms/telegram_room_bootstrap.py-1491-                raise BootstrapConflict("one Telegram user cannot claim both roles")
gateway/platforms/telegram_room_bootstrap.py-1492-            claims[role] = RoleClaim(role, user, chat, topic, message)
gateway/platforms/telegram_room_bootstrap.py-1493-            ordered = tuple(sorted(claims.values(), key=lambda claim: claim.role.value))
gateway/platforms/telegram_room_bootstrap.py-1494-            return replace(session, role_claims=ordered, updated_at=now)
gateway/platforms/telegram_room_bootstrap.py-1495-        return self._mutate(session_id, expected_generation, mutate)
gateway/platforms/telegram_room_bootstrap.py-1496-
gateway/platforms/telegram_room_bootstrap.py:1497:    def expire_unbound(self) -> tuple[RoomBootstrapSession, ...]:
gateway/platforms/telegram_room_bootstrap.py-1498-        now = self._utc_now()
gateway/platforms/telegram_room_bootstrap.py-1499-        with self._locked():
gateway/platforms/telegram_room_bootstrap.py-1500-            sessions = list(self._read_unlocked())
gateway/platforms/telegram_room_bootstrap.py-1501-            updated = self._expire_unbound_values(sessions, now)
gateway/platforms/telegram_room_bootstrap.py-1502-            expired = tuple(
gateway/platforms/telegram_room_bootstrap.py-1503-                after for before, after in zip(sessions, updated)
gateway/platforms/telegram_room_bootstrap.py:1504:                if before.state is not BootstrapState.EXPIRED and after.state is BootstrapState.EXPIRED
gateway/platforms/telegram_room_bootstrap.py-1505-            )
gateway/platforms/telegram_room_bootstrap.py-1506-            if expired:
gateway/platforms/telegram_room_bootstrap.py-1507-                self._write_unlocked(updated)
gateway/platforms/telegram_room_bootstrap.py-1508-            return expired
gateway/platforms/telegram_room_bootstrap.py-1509-
gateway/platforms/telegram_room_bootstrap.py-1510-    def _mutate(
gateway/platforms/telegram_room_bootstrap.py-1511-        self,
gateway/platforms/telegram_room_bootstrap.py-1512-        session_id: str,
gateway/platforms/telegram_room_bootstrap.py-1513-        expected_generation: int,
gateway/platforms/telegram_room_bootstrap.py-1514-        operation: Callable[[RoomBootstrapSession, datetime], RoomBootstrapSession],
gateway/platforms/telegram_room_bootstrap.py-1515-    ) -> RoomBootstrapSession:
gateway/platforms/telegram_room_bootstrap.py-1516-        now = self._utc_now()
gateway/platforms/telegram_room_bootstrap.py-1517-        with self._locked():
gateway/platforms/telegram_room_bootstrap.py-1518-            sessions = list(self._read_unlocked())
gateway/platforms/telegram_room_bootstrap.py-1519-            expired_sessions = self._expire_unbound_values(sessions, now)
gateway/platforms/telegram_room_bootstrap.py-1520-            if expired_sessions != sessions:
gateway/platforms/telegram_room_bootstrap.py-1521-                sessions = expired_sessions
gateway/platforms/telegram_room_bootstrap.py-1522-                self._write_unlocked(sessions)
gateway/platforms/telegram_room_bootstrap.py-1523-            current = self._find(sessions, session_id)
gateway/platforms/telegram_room_bootstrap.py-1524-            self._check_generation(current, expected_generation)
gateway/platforms/telegram_room_bootstrap.py-1525-            updated = operation(current, now)
gateway/platforms/telegram_room_bootstrap.py-1526-            self._replace_session(sessions, updated)
gateway/platforms/telegram_room_bootstrap.py-1527-            self._write_unlocked(sessions)
gateway/platforms/telegram_room_bootstrap.py-1528-            return updated
gateway/platforms/telegram_room_bootstrap.py-1529-
gateway/platforms/telegram_room_bootstrap.py-1530-    @staticmethod
gateway/platforms/telegram_room_bootstrap.py-1531-    def _check_generation(session: RoomBootstrapSession, expected: int) -> None:
gateway/platforms/telegram_room_bootstrap.py-1532-        if type(expected) is not int or expected != session.generation:
gateway/platforms/telegram_room_bootstrap.py-1533-            raise GenerationConflict("bootstrap generation is stale")
gateway/platforms/telegram_room_bootstrap.py-1534-
gateway/platforms/telegram_room_bootstrap.py-1535-    @staticmethod
gateway/platforms/telegram_room_bootstrap.py-1536-    def _find(sessions: tuple[RoomBootstrapSession, ...] | list[RoomBootstrapSession], session_id: str) -> RoomBootstrapSession:
gateway/platforms/telegram_room_bootstrap.py-1537-        if _SESSION_RE.fullmatch(session_id) is None:
gateway/platforms/telegram_room_bootstrap.py-1538-            raise BootstrapError("invalid bootstrap session id")
gateway/platforms/telegram_room_bootstrap.py-1539-        matches = [session for session in sessions if session.session_id == session_id]
gateway/platforms/telegram_room_bootstrap.py-1540-        if len(matches) != 1:
gateway/platforms/telegram_room_bootstrap.py-1541-            raise BootstrapError("bootstrap session is unavailable")
gateway/platforms/telegram_room_bootstrap.py-1542-        return matches[0]
gateway/platforms/telegram_room_bootstrap.py-1543-
gateway/platforms/telegram_room_bootstrap.py-1544-    @staticmethod
gateway/platforms/telegram_room_bootstrap.py-1545-    def _replace_session(sessions: list[RoomBootstrapSession], updated: RoomBootstrapSession) -> None:
gateway/platforms/telegram_room_bootstrap.py-1546-        for index, candidate in enumerate(sessions):
gateway/platforms/telegram_room_bootstrap.py-1547-            if candidate.session_id == updated.session_id:
gateway/platforms/telegram_room_bootstrap.py-1548-                sessions[index] = updated
gateway/platforms/telegram_room_bootstrap.py-1549-                return
gateway/platforms/telegram_room_bootstrap.py-1550-        raise BootstrapError("bootstrap session is unavailable")
gateway/platforms/telegram_room_bootstrap.py-1551-
gateway/platforms/telegram_room_bootstrap.py-1552-    @staticmethod
gateway/platforms/telegram_room_bootstrap.py:1553:    def _expire_unbound_values(sessions: list[RoomBootstrapSession], now: datetime) -> list[RoomBootstrapSession]:
gateway/platforms/telegram_room_bootstrap.py-1554-        return [
gateway/platforms/telegram_room_bootstrap.py:1555:            replace(item, state=BootstrapState.EXPIRED, generation=item.generation + 1, updated_at=now)
gateway/platforms/telegram_room_bootstrap.py-1556-            if not item.terminal and not item.bound and now >= item.expires_at
gateway/platforms/telegram_room_bootstrap.py-1557-            else item
gateway/platforms/telegram_room_bootstrap.py-1558-            for item in sessions
gateway/platforms/telegram_room_bootstrap.py-1559-        ]
gateway/platforms/telegram_room_bootstrap.py-1560-
gateway/platforms/telegram_room_bootstrap.py-1561-    def _prepare_paths(self) -> None:
gateway/platforms/telegram_room_bootstrap.py-1562-        _reject_symlink_chain(self.state_dir)
gateway/platforms/telegram_room_bootstrap.py-1563-        self.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
gateway/platforms/telegram_room_bootstrap.py-1564-        _reject_symlink(self.state_dir, "bootstrap state directory")
gateway/platforms/telegram_room_bootstrap.py-1565-        if not self.state_dir.is_dir():
gateway/platforms/telegram_room_bootstrap.py-1566-            raise BootstrapError("bootstrap state path must be a directory")
gateway/platforms/telegram_room_bootstrap.py-1567-        self.state_dir.chmod(0o700)
gateway/platforms/telegram_room_bootstrap.py-1568-        for path in (self.ledger_path, self.lock_path):
gateway/platforms/telegram_room_bootstrap.py-1569-            if path.exists() or path.is_symlink():
gateway/platforms/telegram_room_bootstrap.py-1570-                _reject_symlink(path, "bootstrap ledger component")
gateway/platforms/telegram_room_bootstrap.py-1571-        descriptor = os.open(
gateway/platforms/telegram_room_bootstrap.py-1572-            self.lock_path,
gateway/platforms/telegram_room_bootstrap.py-1573-            os.O_RDWR | os.O_CREAT | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
gateway/platforms/telegram_room_bootstrap.py-1574-            0o600,
gateway/platforms/telegram_room_bootstrap.py-1575-        )
gateway/platforms/telegram_room_bootstrap.py-1576-        try:
gateway/platforms/telegram_room_bootstrap.py-1577-            os.fchmod(descriptor, 0o600)
gateway/platforms/telegram_room_bootstrap.py-1578-        finally:
gateway/platforms/telegram_room_bootstrap.py-1579-            os.close(descriptor)
gateway/platforms/telegram_room_bootstrap.py-1580-
gateway/platforms/telegram_room_bootstrap.py-1581-    @contextmanager
gateway/platforms/telegram_room_bootstrap.py-1582-    def _locked(self) -> Iterator[None]:
gateway/platforms/telegram_room_bootstrap.py-1583-        descriptor = os.open(
gateway/platforms/telegram_room_bootstrap.py-1584-            self.lock_path,
gateway/platforms/telegram_room_bootstrap.py-1585-            os.O_RDWR | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
gateway/platforms/telegram_room_bootstrap.py-1586-        )
gateway/platforms/telegram_room_bootstrap.py-1587-        try:
gateway/platforms/telegram_room_bootstrap.py-1588-            info = os.fstat(descriptor)
gateway/platforms/telegram_room_bootstrap.py-1589-            if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
gateway/platforms/telegram_room_bootstrap.py-1590-                raise BootstrapError("bootstrap lock must be a regular file")
gateway/platforms/telegram_room_bootstrap.py-1591-            os.fchmod(descriptor, 0o600)
gateway/platforms/telegram_room_bootstrap.py-1592-            fcntl.flock(descriptor, fcntl.LOCK_EX)
gateway/platforms/telegram_room_bootstrap.py-1593-            yield
gateway/platforms/telegram_room_bootstrap.py-1594-        finally:
gateway/platforms/telegram_room_bootstrap.py-1595-            fcntl.flock(descriptor, fcntl.LOCK_UN)
gateway/platforms/telegram_room_bootstrap.py-1596-            os.close(descriptor)
gateway/platforms/telegram_room_bootstrap.py-1597-
gateway/platforms/telegram_room_bootstrap.py-1598-    def _read_unlocked(self) -> tuple[RoomBootstrapSession, ...]:
gateway/platforms/telegram_room_bootstrap.py-1599-        _reject_symlink(self.ledger_path, "bootstrap ledger")
gateway/platforms/telegram_room_bootstrap.py-1600-        descriptor = os.open(
gateway/platforms/telegram_room_bootstrap.py-1601-            self.ledger_path,
gateway/platforms/telegram_room_bootstrap.py-1602-            os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
gateway/platforms/telegram_room_bootstrap.py-1603-        )
gateway/platforms/telegram_room_bootstrap.py-1604-        try:
gateway/platforms/telegram_room_bootstrap.py-1605-            info = os.fstat(descriptor)
gateway/platforms/telegram_room_bootstrap.py-1606-            if (
gateway/platforms/telegram_room_bootstrap.py-1607-                not stat.S_ISREG(info.st_mode)
gateway/platforms/telegram_room_bootstrap.py-1608-                or info.st_nlink != 1
gateway/platforms/telegram_room_bootstrap.py-1609-                or info.st_mode & 0o077
gateway/platforms/telegram_room_bootstrap.py-1610-            ):
gateway/platforms/telegram_room_bootstrap.py-1611-                raise BootstrapError("bootstrap ledger permissions are unsafe")
gateway/platforms/telegram_room_bootstrap.py-1612-            raw = os.read(descriptor, info.st_size + 1)
gateway/platforms/telegram_room_bootstrap.py-1613-        finally:
gateway/platforms/telegram_room_bootstrap.py-1614-            os.close(descriptor)
gateway/platforms/telegram_room_bootstrap.py-1615-        try:
gateway/platforms/telegram_room_bootstrap.py-1616-            value = json.loads(raw.decode("utf-8"))
gateway/platforms/telegram_room_bootstrap.py-1617-        except (UnicodeDecodeError, json.JSONDecodeError) as exc:
gateway/platforms/telegram_room_bootstrap.py-1618-            raise BootstrapError("bootstrap ledger is invalid") from exc
gateway/platforms/telegram_room_bootstrap.py-1619-        if not isinstance(value, dict) or set(value) != {"schema", "sessions", "payload_digest"}:
gateway/platforms/telegram_room_bootstrap.py-1620-            raise BootstrapError("bootstrap ledger envelope is invalid")
gateway/platforms/telegram_room_bootstrap.py-1621-        payload = {"schema": value.get("schema"), "sessions": value.get("sessions")}
gateway/platforms/telegram_room_bootstrap.py-1622-        digest = value.get("payload_digest")
gateway/platforms/telegram_room_bootstrap.py-1623-        if value.get("schema") != _SCHEMA or not isinstance(digest, str) or not hmac.compare_digest(digest, _object_digest(payload)):
gateway/platforms/telegram_room_bootstrap.py-1624-            raise BootstrapError("bootstrap ledger digest mismatch")
gateway/platforms/telegram_room_bootstrap.py-1625-        rows = value.get("sessions")
gateway/platforms/telegram_room_bootstrap.py-1626-        if not isinstance(rows, list):
gateway/platforms/telegram_room_bootstrap.py-1627-            raise BootstrapError("bootstrap ledger sessions are invalid")
gateway/platforms/telegram_room_bootstrap.py-1628-        sessions = tuple(_session_from_dict(row) for row in rows)
gateway/platforms/telegram_room_bootstrap.py-1629-        if len({session.session_id for session in sessions}) != len(sessions):
gateway/platforms/telegram_room_bootstrap.py-1630-            raise BootstrapError("bootstrap ledger contains duplicate sessions")
gateway/platforms/telegram_room_bootstrap.py-1631-        return sessions
gateway/platforms/telegram_room_bootstrap.py-1632-
gateway/platforms/telegram_room_bootstrap.py-1633-    def _write_unlocked(self, sessions: tuple[RoomBootstrapSession, ...] | list[RoomBootstrapSession]) -> None:
gateway/platforms/telegram_room_bootstrap.py-1634-        rows = [_session_to_dict(session) for session in sessions]
gateway/platforms/telegram_room_bootstrap.py-1635-        payload: dict[str, object] = {"schema": _SCHEMA, "sessions": rows}
gateway/platforms/telegram_room_bootstrap.py-1636-        envelope = {**payload, "payload_digest": _object_digest(payload)}
gateway/platforms/telegram_room_bootstrap.py-1637-        data = (_canonical_json(envelope) + "\n").encode("utf-8")
gateway/platforms/telegram_room_bootstrap.py-1638-        descriptor, name = tempfile.mkstemp(prefix=".ledger.", dir=self.state_dir)
gateway/platforms/telegram_room_bootstrap.py-1639-        temporary = Path(name)
gateway/platforms/telegram_room_bootstrap.py-1640-        try:
gateway/platforms/telegram_room_bootstrap.py-1641-            os.fchmod(descriptor, 0o600)
gateway/platforms/telegram_room_bootstrap.py-1642-            with os.fdopen(descriptor, "wb", closefd=True) as handle:
gateway/platforms/telegram_room_bootstrap.py-1643-                handle.write(data)
gateway/platforms/telegram_room_bootstrap.py-1644-                handle.flush()
gateway/platforms/telegram_room_bootstrap.py-1645-                os.fsync(handle.fileno())

[Showing lines 187-752 of 752 (50.0KB limit). Full output: /tmp/pi-bash-d79fe6fc07256b0f.log]


Task22/activation tests that establish invalid/stale/duplicate behavior:
tests/gateway/test_telegram_nutrition_onboarding.py:550:def test_ready_card_announces_activation_readiness_to_owner() -> None:
tests/gateway/test_telegram_nutrition_onboarding.py:606:def test_wrong_or_stale_evidence_is_consumed_without_mutation(
tests/gateway/test_telegram_nutrition_onboarding.py:668:def test_committed_publication_is_reused_after_restart() -> None:
tests/gateway/test_telegram_nutrition_onboarding.py:1949:def test_rewind_invalidates_changed_question_and_all_dependents() -> None:
tests/gateway/test_telegram_room_bootstrap_cutover.py:522:def test_provider_authentication_preflight_blocks_activation_without_mutation(
tests/gateway/test_telegram_room_bootstrap_cutover.py:542:def test_execute_uses_canonical_cutover_then_completion_and_replay_is_idempotent(
tests/gateway/test_telegram_room_bootstrap_cutover.py:598:def test_completed_replay_authentication_failure_is_strictly_non_mutating(
tests/gateway/test_telegram_room_bootstrap_cutover.py:621:def test_completed_replay_with_matching_invalid_current_selectors_is_non_mutating(
tests/gateway/test_telegram_room_bootstrap_cutover.py:664:def test_completed_replay_with_invalid_readiness_receipt_disables_legacy_target(
tests/gateway/test_telegram_room_bootstrap_cutover.py:709:def test_target_activated_rejects_preceding_wrong_readiness_generation(
tests/gateway/test_telegram_room_bootstrap_cutover.py:732:def test_completed_replay_requires_immediately_preceding_readiness_generation(
tests/gateway/test_telegram_room_bootstrap_cutover.py:752:def test_replay_recovers_after_current_disable(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
tests/gateway/test_telegram_room_bootstrap_cutover.py:801:def test_replay_without_durable_readiness_receipt_disables_activated_target(
tests/gateway/test_telegram_room_bootstrap_cutover.py:818:def test_replay_recovers_after_activation_commit_without_duplicate_activation(
tests/gateway/test_telegram_room_bootstrap_cutover.py:932:def test_unsafe_topology_is_rejected_before_activation_and_target_stays_disabled(
tests/gateway/test_telegram_room_bootstrap_cutover.py:1007:def test_preflight_refuses_true_delivery_or_activation_flags(tmp_path: Path, flag: str) -> None:
tests/gateway/test_telegram_room_bootstrap_cutover.py:1018:def test_preflight_refuses_stale_or_non_customer_specific_checklist(tmp_path: Path) -> None:
tests/gateway/test_task22_supplemental_owner_review.py:342:def test_default_preflight_verifies_terminal_replay_read_only(
tests/gateway/test_task22_supplemental_owner_review.py:701:def test_resigned_float_review_update_cannot_restart_as_committed(
tests/gateway/test_task22_supplemental_owner_review.py:789:def test_valid_post_review_generation28_boundary_restarts_exactly_once(
tests/gateway/test_task22_supplemental_owner_review.py:846:def test_happy_path_is_append_only_and_task23_gate_matches_bundle(tmp_path: Path) -> None:
tests/gateway/test_task22_supplemental_owner_review.py:1341:def test_task23_gate_rejects_incomplete_supplemental_approval(
tests/gateway/test_task22_supplemental_owner_review.py:1357:def test_restart_pending_does_not_create_or_duplicate_publication(tmp_path: Path) -> None:
tests/gateway/test_task22_supplemental_owner_review.py:1827:def test_recovery_launcher_rejects_wrong_sentinel_without_mutation(
tests/gateway/test_task22_supplemental_owner_review.py:2029:def test_duplicate_stale_and_wrong_authority_do_not_change_journal(tmp_path: Path) -> None:
tests/gateway/test_task22_supplemental_owner_review.py:2101:def test_replay_rejects_changed_baseline_bytes_with_declared_digest_and_inode_preserved(
tests/gateway/test_task22_supplemental_owner_review.py:2217:def test_preparation_rejects_activation_delivery_and_trainer_authority(
Test tool availability:
pytest 9.0.2


bootstrap_ledger e3271199c1cb38476c44c55fe5318f5543a4152b4717033677e0c1c65f4971da 1786530232354554527 33165847
registry 69c1382eb663942a7c8c18b9fee29141612ac37e92f1ffb7df0ba1beed44579d 1786481043085831188 30576943
task22_journal f5fc5eebff8970c72270e05b56f88baa83d69404ef4098bacae6ba2ef8af612e 1786527367078034729 33165817
phase= {'phase': 'PUBLICATION_COMMITTED', 'sequence': 2} review_update_id= 629525051
outbox 9d8ccbb1b5268733c107f1eb631dda7e45c15e6302fb9c41a66e710b2f2db2d2 1786527367066813137 33165816
generations= [(0, 'customer', 'COMMITTED', 74), (1, 'customer', 'COMMITTED', 76), (2, 'customer', 'COMMITTED', 78), (3, 'customer', 'COMMITTED', 80), (4, 'customer', 'COMMITTED', 82), (5, 'customer', 'COMMITTED', 84), (6, 'customer', 'COMMITTED', 86), (7, 'customer', 'COMMITTED', 88), (8, 'customer', 'COMMITTED', 90), (9, 'customer', 'COMMITTED', 92), (10, 'customer', 'COMMITTED', 94), (11, 'customer', 'COMMITTED', 96), (12, 'customer', 'COMMITTED', 98), (13, 'customer', 'COMMITTED', 100), (14, 'customer', 'COMMITTED', 102), (15, 'customer', 'COMMITTED', 104), (16, 'customer', 'COMMITTED', 106), (17, 'customer', 'COMMITTED', 108), (18, 'customer', 'COMMITTED', 110), (19, 'customer', 'COMMITTED', 112), (20, 'customer', 'COMMITTED', 114), (21, 'customer', 'COMMITTED', 116), (23, 'customer', 'COMMITTED', 118), (24, 'trainer', 'COMMITTED', 13), (25, 'owner', 'COMMITTED', 120), (26, 'owner', 'COMMITTED', 121), (27, 'owner', 'COMMITTED', 122), (28, 'owner', 'COMMITTED', 123)]
callbacks b2b4e23c98c2f982df4e18fa254f8520b4d5f47d2613831b36f927a398c47ced 1786524363931691426 33165840
callback_records= 1
Current service journal tail (read-only):
-- No entries --


**Identifying Task22 ledger mismatch invalidation**

**Investigating require_task22 function details**

**Planning safe test execution for activation**

**Investigating preflight CLI for verification**

**Verifying script side effects and state mutations**

**Identifying manual blocker for bootstrap reset**

bash

cd /home/cube/projects/richard/hermes-agent && PYTHONDONTWRITEBYTECODE=1 .venv/bin/python scripts/task22-supplemental-owner-review --preflight

set -eu
root=/home/cube/.hermes/profiles/dualcoachtest
python3 - <<'PY'
from pathlib import Path
import json,hashlib
root=Path('/home/cube/.hermes/profiles/dualcoachtest')
journal=json.loads((root/'data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/task22-supplemental-owner-review-v1.json').read_text())
ledger=(root/'data/onboarding/telegram-room-bootstrap-v1/ledger.json').read_bytes()
sealed=journal['sealed_sources']
for name,item in sealed.items():
 if name.endswith('telegram-room-bootstrap-v1/ledger.json'):
  print(json.dumps({'sealed_path':name,'sealed_sha256':item.get('sha256'),'current_sha256':hashlib.sha256(ledger).hexdigest(),'sealed_file_identity':item.get('file_identity'),'current_identity':{'device':ledger and (root/'data/onboarding/telegram-room-bootstrap-v1/ledger.json').stat().st_dev,'inode':(root/'data/onboarding/telegram-room-bootstrap-v1/ledger.json').stat().st_ino,'size':len(ledger)}},sort_keys=True))
print('journal_bootstrap_digest=',journal['bootstrap_digest'])
print('current_bootstrap_digest=',hashlib.sha256(ledger).hexdigest())
PY
# Audit-only journal metadata and profile scope.
printf 'systemd state: '; systemctl --user is-active hermes-gateway-dualcoachtest.service
printf 'activation artifacts: '; find "$root/data" -maxdepth 3 \( -name 'customer-activation-journal.json' -o -name 'scheduled-deliveries.jsonl' -o -name 'draft-deliveries.json' \) -printf '%p\n' | wc -l
printf 'task22 supplemental journal mtime: '; stat -c '%y' "$root/data/customers/task22_dm_rehearsal/nutrition-onboarding/migrations/task22-supplemental-owner-review-v1.json"

cd /home/cube/projects/richard/hermes-agent && PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -q -p no:cacheprovider \
  tests/gateway/test_task22_supplemental_owner_review.py::test_task23_gate_rejects_incomplete_supplemental_approval \
  tests/gateway/test_task22_supplemental_owner_review.py::test_duplicate_stale_and_wrong_authority_do_not_change_journal \
  tests/gateway/test_task22_supplemental_owner_review.py::test_restart_pending_does_not_create_or_duplicate_publication \
  tests/gateway/test_telegram_room_bootstrap_cutover.py::test_provider_authentication_preflight_blocks_activation_without_mutation \
  tests/gateway/test_telegram_room_bootstrap_cutover.py::test_replay_recovers_after_activation_commit_without_duplicate_activation


        raise SupplementalReviewError("supplemental review journal discovery failed") from exc
    if pins.customer_key != customer_key or pins.session_id != session_id or not isinstance(manifest_path, str):
        raise SupplementalReviewError("supplemental review journal discovery mismatched authority")
    paths = SupplementalReviewPaths.canonical(
        root,
        candidate_manifest=manifest_path,
        customer_key=customer_key,
    )
    return SupplementalReviewStore(paths, pins, outbox=outbox)

def require_task22_supplemental_for_activation(
    profile_root: Path | str,
    *,
    customer_key: str,
    session_id: str,
) -> bool:
    """Require the overlay when this exact bounded Task22 journal is present."""
    root = Path(profile_root)
    onboarding = root / "data" / "customers" / customer_key / "nutrition-onboarding"
    journal = onboarding / "migrations" / _JOURNAL_NAME
    if not journal.exists():
        return False
    baseline = _object(_read_private(onboarding / "baseline-v1.json", "baseline"), "baseline")
    baseline_digest = baseline.get("digest")
    if not isinstance(baseline_digest, str):
        raise SupplementalReviewError("activation baseline digest is invalid")
    readiness_bundle_digest = _digest({
        "ready": _sha(_read_private(onboarding / "ready.json", "ready bundle")),
        "pointer": _sha(_read_private(onboarding / "readiness-current.json", "readiness pointer")),
        "receipt": _sha(_read_private(onboarding / "readiness-receipt-v1.json", "readiness receipt")),
    })
    require_supplemental_owner_approval(
        root,
        customer_key=customer_key,
        session_id=session_id,
        baseline_digest=baseline_digest,
        readiness_bundle_digest=readiness_bundle_digest,
    )
    return True

def require_supplemental_owner_approval(
    profile_root: Path | str,
    *,
    customer_key: str,
    session_id: str,
    baseline_digest: str,
    readiness_bundle_digest: str,
) -> None:
    """Task23 gate: require the completed overlay for the identical ready bundle."""
    journal = (
        Path(profile_root) / "data" / "customers" / customer_key / "nutrition-onboarding"
        / "migrations" / _JOURNAL_NAME
    )
    document = _supplemental_journal(_read_private(journal, "supplemental review journal"))
    key_path = (
        Path(profile_root) / "data" / "onboarding"
        / "telegram-publication-outbox-v1" / ".receipt-key"
    )
    key = _read_private(key_path, "publication receipt key")
    integrity = document.get("integrity")
    unsigned = dict(document)
    unsigned.pop("integrity", None)
    valid_integrity = isinstance(integrity, str) and hmac.compare_digest(
        integrity,
        hmac.new(key, _canonical(unsigned), hashlib.sha256).hexdigest(),
    )
    outbox = GatewayOnboardingPublicationOutbox(profile_root)
    event = outbox.owner_callback(session_id)
    records = outbox.records()
    review_matches = tuple(
        item for item in records
        if item.session_id == session_id and item.generation == 27
        and item.role == "owner" and item.state == "COMMITTED"
        and item.payload.get("state") == "owner_review"
    )
    terminal_matches = tuple(
        item for item in records
        if item.session_id == session_id and item.generation == 28
        and item.role == "owner" and item.state == "COMMITTED"
        and item.payload.get("state") == "ready"
    )
    phases = document.get("phase_events")
    if (
        not valid_integrity
        or document.get("schema") != _SCHEMA
        or document.get("customer_key") != customer_key
        or document.get("session_id") != session_id
        or document.get("baseline_digest") != baseline_digest
        or document.get("readiness_bundle_digest") != readiness_bundle_digest
        or phases != [
            {"phase": SupplementalReviewPhase.PREPARED.value, "sequence": 0},
            {"phase": SupplementalReviewPhase.REVIEW_COMMITTED.value, "sequence": 1},
            {"phase": SupplementalReviewPhase.PUBLICATION_COMMITTED.value, "sequence": 2},
        ]
        or event is None
        or document.get("review_update_id") != event.update_id
        or document.get("callback_event_integrity") != event.event_integrity
        or len(review_matches) != 1
        or len(terminal_matches) != 1
        or event.publication_generation != 27
        or event.publication_dispatch_identity != review_matches[0].dispatch_identity
        or event.publication_receipt_integrity != review_matches[0].receipt_integrity
        or document.get("terminal_receipt_integrity") != terminal_matches[0].receipt_integrity
        or document.get("terminal_publication_binding")
        != _publication_binding(terminal_matches[0])
        or terminal_matches[0].payload.get("supplemental_review_binding")
        != {
            "schema": _TERMINAL_BINDING_SCHEMA,
            "transaction_id": document.get("transaction_id"),
            "review_commit_digest": document.get("review_commit_digest"),
            "callback_envelope_digest": document.get("callback_envelope_digest"),
            "callback_event_integrity": document.get("callback_event_integrity"),
            "review_update_id": document.get("review_update_id"),
            "authority_digest": document.get("authority_digest"),
            "review_sequence": 1,
            "publication_sequence": 2,
        }
        or not isinstance(document.get("approval_digest"), str)

[348 more lines in file. Use offset=2000 to continue.]


#!/usr/bin/env python3
"""Verify or execute the sole Task22 historical terminal recovery."""

from __future__ import annotations

import argparse
import asyncio
import fcntl
import hashlib
import importlib
import json
import os
import stat
import sys
import tomllib
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Iterator, cast

sys.dont_write_bytecode = True

_PROFILE = "dualcoachtest"
_CUSTOMER = "task22_dm_rehearsal"
_SESSION = "rb_Ar-NuKG3LH8CqHPXUgc88g"
_PROFILE_ROOT = Path("/home/cube/.hermes/profiles/dualcoachtest")
_MANIFEST = Path(
    "/home/cube/projects/richard/traning coach/.omo/evidence/dualcoach-task22-supplemental-successor-manifest.json"
)
_GATEWAY_ROOT = Path("/home/cube/projects/richard/hermes-agent")
_CHECKIN_PACKAGE_ROOT = (
    _PROFILE_ROOT / "workspace" / "checkin_cli"
)
_CHECKIN_PACKAGE_DIGEST = (
    "3090dc7ca9e93236394dd4da88647b100ea70b22f00080950a24e1d254f575e6"
)
_CHECKIN_PACKAGE_VERSION = "0.1.0"
_CHECKIN_API_VERSION = "2.0"
_RECOVERY_SENTINEL = (
    "task22-terminal-recovery-v1:629525051:"
    "987cdde4e245b1d316ab5000271a755db148153b5207156b8082f97c2a1f5c18"
)
_CALLBACK_EVENT_INTEGRITY = (
    "987cdde4e245b1d316ab5000271a755db148153b5207156b8082f97c2a1f5c18"
)
_UPDATE_ID = 629525051
_MESSAGE_ID = 122
_OWNER_AUTHORITY = (8693203710, 8693203710, 0)
_OWNER_ROUTE = ("8693203710", "0")
_READY_TEXT = (
    "온보딩 자료와 준비 검증이 완료됐습니다. "
    "운영자 활성화 전까지 고객 전달은 비활성 상태입니다."
)

def _read_json(path: Path) -> dict[str, object]:
    flags = (
        os.O_RDONLY
        | os.O_CLOEXEC
        | getattr(os, "O_NOFOLLOW", 0)
        | getattr(os, "O_NOATIME", 0)
    )
    descriptor = os.open(path, flags)
    try:
        info = os.fstat(descriptor)
        if (
            not stat.S_ISREG(info.st_mode)
            or info.st_nlink != 1
            or stat.S_IMODE(info.st_mode) != 0o600
        ):
            raise ValueError(f"private JSON authority is invalid: {path}")
        with os.fdopen(descriptor, "r", encoding="utf-8", closefd=False) as handle:
            value = json.load(handle)
    finally:
        os.close(descriptor)
    if not isinstance(value, dict):
        raise ValueError(f"JSON authority is invalid: {path}")
    return value

def _bind_exact_checkin_cli() -> None:
    manifest = _read_json(_MANIFEST)
    roots = manifest.get("roots")
    leaves = manifest.get("candidate_files")
    if (
        not isinstance(roots, dict)
        or roots.get("profile_package") != str(_CHECKIN_PACKAGE_ROOT)
        or manifest.get("profile_package_digest") != _CHECKIN_PACKAGE_DIGEST
        or not isinstance(leaves, list)
    ):
        raise ValueError("sealed checkin_cli package authority does not match")
    try:
        root_info = _CHECKIN_PACKAGE_ROOT.lstat()
    except OSError as exc:
        raise ValueError("sealed checkin_cli package root is unavailable") from exc
    if (
        stat.S_ISLNK(root_info.st_mode)
        or not stat.S_ISDIR(root_info.st_mode)
        or stat.S_IMODE(root_info.st_mode) != 0o700
    ):
        raise ValueError("sealed checkin_cli package root is invalid")
    package = hashlib.sha256()
    paths: list[str] = []
    pyproject_content: bytes | None = None
    for raw in leaves:
        if not isinstance(raw, dict) or raw.get("root") != "profile_package":
            continue
        relative = raw.get("path")
        expected_bytes = raw.get("bytes")
        expected_sha256 = raw.get("sha256")
        if (
            not isinstance(relative, str)
            or not relative
            or Path(relative).is_absolute()
            or ".." in Path(relative).parts
            or type(expected_bytes) is not int
            or not isinstance(expected_sha256, str)
        ):
            raise ValueError("sealed checkin_cli package leaf is invalid")
        path = _CHECKIN_PACKAGE_ROOT / relative
        descriptor = os.open(
            path,
            os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0)
            | getattr(os, "O_NOATIME", 0),
        )
        try:
            info = os.fstat(descriptor)
            chunks: list[bytes] = []
            while chunk := os.read(descriptor, 1 << 20):
                chunks.append(chunk)
        finally:
            os.close(descriptor)
        content = b"".join(chunks)
        if (
            not stat.S_ISREG(info.st_mode)
            or info.st_nlink != 1
            or len(content) != expected_bytes
            or hashlib.sha256(content).hexdigest() != expected_sha256
        ):
            raise ValueError(f"sealed checkin_cli package drift: {relative}")
        package.update(relative.encode() + b"\0" + content + b"\0")
        paths.append(relative)
        if relative == "pyproject.toml":
            pyproject_content = content
    if (
        not paths
        or pyproject_content is None
        or package.hexdigest() != _CHECKIN_PACKAGE_DIGEST
    ):
        raise ValueError("sealed checkin_cli package digest does not match")
    pyproject = tomllib.loads(pyproject_content.decode("utf-8"))
    project = pyproject.get("project")
    if (
        not isinstance(project, dict)
        or project.get("name") != "physique-checkin-cli"
        or project.get("version") != _CHECKIN_PACKAGE_VERSION
    ):
        raise ValueError("sealed checkin_cli package version does not match")
    package_text = str(_CHECKIN_PACKAGE_ROOT)
    if package_text in sys.path:
        sys.path.remove(package_text)
    sys.path.insert(0, package_text)
    domain = importlib.import_module("checkin_cli.nutrition_onboarding")
    source = Path(str(getattr(domain, "__file__", ""))).resolve()
    if (
        source != (_CHECKIN_PACKAGE_ROOT / "checkin_cli" / "nutrition_onboarding.py")
        or getattr(domain, "NUTRITION_ONBOARDING_API_VERSION", None)
        != _CHECKIN_API_VERSION
    ):
        raise ValueError("sealed checkin_cli runtime import does not match")

@contextmanager
def _hold_gateway_runtime_lock(profile_root: Path) -> Iterator[None]:
    path = profile_root / "gateway.lock"
    descriptor = os.open(
        path,
        os.O_RDWR | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
    )
    try:
        info = os.fstat(descriptor)
        if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
            raise ValueError("target gateway runtime lock is invalid")
        try:
            fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError as exc:
            raise ValueError("target gateway service is active") from exc
        yield
    finally:
        os.close(descriptor)

class _ExactGeneration28Adapter:
    def __init__(self, bot: Any) -> None:
        self._bot = bot
        self.send_count = 0

    async def _send_nutrition_topic(
        self,
        *,
        chat_id: object,
        topic_id: object,
        **kwargs: object,
    ) -> object:
        if (
            (str(chat_id), str(topic_id)) != _OWNER_ROUTE
            or kwargs != {"text": _READY_TEXT}
            or self.send_count != 0
        ):
            raise ValueError("exact Task22 generation-28 publication is not authorized")
        self.send_count += 1
        return await self._bot.send_message(chat_id=chat_id, text=_READY_TEXT)

async def _advance_exact_terminal(
    *,
    store: Any,
    runtime: Any,
    session: Any,
    service: Any,
    authority: tuple[int, int, int],
    route: tuple[str, str],
    checkpoint: Any = None,
) -> Any:
    from gateway.platforms.telegram_nutrition_onboarding_supplemental_review import (
        SupplementalPublicationStatus,
        SupplementalReviewPhase,
    )

    snapshot, event = store.authorize_exact_task22_terminal_recovery(
        authority=authority,
        route=route,
    )
    if snapshot.phase is SupplementalReviewPhase.PREPARED:
        snapshot = store.commit_review(
            event=event,
            authority=authority,
            route=route,
        )
    if checkpoint is not None:
        checkpoint("review_committed")
    if snapshot.phase is SupplementalReviewPhase.REVIEW_COMMITTED:
        await runtime._publish(
            session,
            service,
            SupplementalPublicationStatus.ready(),
        )
        if checkpoint is not None:
            checkpoint("publication_committed")
        snapshot = store.commit_publication(authority=authority, route=route)
    if checkpoint is not None:
        checkpoint("terminal_committed")
    if snapshot.phase is not SupplementalReviewPhase.PUBLICATION_COMMITTED:
        raise ValueError("exact Task22 terminal recovery did not reach terminal state")
    return snapshot

def _load_exact_runtime_imports() -> tuple[Any, Any, Any, Any, Any, Any]:
    _bind_exact_checkin_cli()
    from agent.secret_scope import build_profile_secret_scope
    from gateway.platforms.telegram_nutrition_onboarding_runtime import (
        TelegramNutritionOnboardingRuntime,
    )
    from gateway.platforms.telegram_room_bootstrap import (
        RoomBootstrapStore,
        RoomBootstrapTransport,
        room_bootstrap_state_dir,
    )
    from telegram import Bot

    return (
        build_profile_secret_scope,
        TelegramNutritionOnboardingRuntime,
        RoomBootstrapStore,
        RoomBootstrapTransport,
        room_bootstrap_state_dir,
        Bot,
    )

async def _recover_exact(
    store: Any,
    authority: tuple[int, int, int],
    route: tuple[str, str],
) -> tuple[Any, int]:
    (
        build_profile_secret_scope,
        TelegramNutritionOnboardingRuntime,
        RoomBootstrapStore,
        RoomBootstrapTransport,
        room_bootstrap_state_dir,
        Bot,
    ) = _load_exact_runtime_imports()
    token = build_profile_secret_scope(_PROFILE_ROOT).get(
        "TELEGRAM_BOT_TOKEN", ""
    ).strip()
    if not token:
        raise ValueError("dedicated Telegram credential is unavailable")
    bootstrap = RoomBootstrapTransport(
        RoomBootstrapStore(room_bootstrap_state_dir(_PROFILE_ROOT)),
        owner_id=str(_OWNER_AUTHORITY[0]),

[178 more lines in file. Use offset=301 to continue.]


        topic_id=script._OWNER_ROUTE[1],
        text=script._READY_TEXT,
    )).message_id == 123
    bot.send_message.assert_awaited_once_with(
        chat_id=script._OWNER_ROUTE[0], text=script._READY_TEXT
    )
    for kwargs in (
        {"chat_id": "wrong", "topic_id": "0", "text": script._READY_TEXT},
        {"chat_id": script._OWNER_ROUTE[0], "topic_id": "0", "text": "wrong"},
        {"chat_id": script._OWNER_ROUTE[0], "topic_id": "0", "text": script._READY_TEXT},
    ):
        with pytest.raises(ValueError, match="not authorized"):
            asyncio.run(adapter._send_nutrition_topic(**kwargs))

def test_gateway_runtime_lock_rejects_active_holder(tmp_path: Path) -> None:
    script = _review_script()
    lock = tmp_path / "gateway.lock"
    lock.write_bytes(b"{}")
    descriptor = os.open(lock, os.O_RDWR | os.O_CLOEXEC)
    try:
        fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
        with pytest.raises(ValueError, match="service is active"):
            with script._hold_gateway_runtime_lock(tmp_path):
                pytest.fail("active gateway lock was accepted")
    finally:
        os.close(descriptor)

def test_duplicate_stale_and_wrong_authority_do_not_change_journal(tmp_path: Path) -> None:
    store, authority, route = _fixture(tmp_path)
    _prepare_and_review(store, authority, route)
    before = store.paths.journal.read_bytes()
    event = store.callback_if_captured()
    assert event is not None
    assert store.commit_review(event=event, authority=authority, route=route).phase is SupplementalReviewPhase.REVIEW_COMMITTED
    assert store.paths.journal.read_bytes() == before
    for wrong_authority, wrong_route in (
        ((13, -100, 22), route),
        (authority, ("-100", "23")),
    ):
        with pytest.raises(SupplementalReviewError, match="authority"):
            store.load(authority=wrong_authority, route=wrong_route)
    assert store.paths.journal.read_bytes() == before

@pytest.mark.parametrize("target", ("manifest", "baseline", "ready", "original", "pointer"))
def test_freeze_drift_and_rebinding_fail_closed(tmp_path: Path, target: str) -> None:
    store, authority, route = _fixture(tmp_path)
    store.prepare(authority=authority, route=route)
    path = {
        "manifest": store.paths.candidate_manifest,
        "baseline": store.paths.baseline_candidate,
        "ready": store.paths.ready,
        "original": store.paths.original_journal,
        "pointer": store.paths.readiness_pointer,
    }[target]
    path.write_bytes(path.read_bytes() + b" ")
    with pytest.raises(SupplementalReviewError, match="drift|candidate"):
        store.load(authority=authority, route=route)

def test_prepare_binds_exact_bytes_length_hash_and_identity_for_every_sealed_source(
    tmp_path: Path,
) -> None:
    store, authority, route = _fixture(tmp_path)

    store.prepare(authority=authority, route=route)

    journal = json.loads(store.paths.journal.read_text(encoding="utf-8"))
    expected_paths = (
        store.paths.candidate_manifest,
        store.paths.baseline_candidate,
        store.paths.baseline,
        store.paths.ready,
        store.paths.readiness_pointer,
        store.paths.readiness_receipt,
        store.paths.original_journal,
        store.paths.registry,
        store.paths.bootstrap,
    )
    expected: dict[str, dict[str, object]] = {}
    for path in expected_paths:
        data = path.read_bytes()
        info = path.stat()
        key = (
            str(path.relative_to(store.paths.profile_root))
            if path.is_relative_to(store.paths.profile_root)
            else f"candidate:{path.name}"
        )
        expected[key] = {
            "file_identity": [info.st_dev, info.st_ino],
            "length": len(data),
            "sha256": hashlib.sha256(data).hexdigest(),
        }

[181 more lines in file. Use offset=2095 to continue.]


    _commit_publication(
        successor_store.outbox,
        generation=28,
        state="ready",
        message_id=123,
    )
    before = _tree_snapshot(successor_store.paths.profile_root)
    with pytest.raises(SupplementalReviewError, match="generation-27 publication"):
        successor_store.preflight(authority=authority, route=route)
    _assert_tree_unchanged(successor_store.paths.profile_root, before)

@pytest.mark.parametrize("phase", ("prepared", "review"))
def test_task23_gate_rejects_incomplete_supplemental_approval(
    tmp_path: Path,
    phase: str,
) -> None:
    store, authority, route = _fixture(tmp_path)
    store.prepare(authority=authority, route=route)
    if phase == "review":
        _prepare_and_review(store, authority, route)
    with pytest.raises(SupplementalReviewError, match="approval"):
        require_task22_supplemental_for_activation(
            store.paths.profile_root,
            customer_key="customer",
            session_id="sid",
        )

def test_restart_pending_does_not_create_or_duplicate_publication(tmp_path: Path) -> None:
    store, authority, route = _fixture(tmp_path)
    first = store.prepare(authority=authority, route=route)
    restarted = discover_supplemental_review_store(
        store.paths.profile_root,
        outbox=GatewayOnboardingPublicationOutbox(store.paths.profile_root),
        customer_key="customer",
        session_id="sid",
    )
    assert restarted is not None
    assert restarted.load(authority=authority, route=route) == first
    assert [item.generation for item in restarted.outbox.records()] == [23, 25, 26]

@pytest.mark.asyncio
async def test_disposable_prepared_restart_publishes_generation27_once_without_generation28(
    tmp_path: Path,
) -> None:
    store, authority, route = _fixture(tmp_path)
    store.prepare(authority=authority, route=route)
    current = SimpleNamespace(
        generation=26,
        payload={"state": "ready"},
        state="COMMITTED",
        message_id=121,
    )

    class PublicationStore:
        def load_session(self, session_id: str) -> SimpleNamespace:
            assert session_id == "sid"
            return current

        def mark_prepared(
            self, *, session_id: str, generation: int, payload: dict[str, object]
        ) -> SimpleNamespace:
            nonlocal current
            assert session_id == "sid" and generation == 27
            if current.generation == generation and current.payload == payload:
                return current
            current = SimpleNamespace(
                generation=generation,

[878 more lines in file. Use offset=1398 to continue.]


usage: task22-supplemental-owner-review [-h] [--recover-exact SENTINEL]
task22-supplemental-owner-review: error: unrecognized arguments: --preflight

Command exited with code 2


{"current_identity": {"device": 66306, "inode": 33165847, "size": 1745}, "current_sha256": "e3271199c1cb38476c44c55fe5318f5543a4152b4717033677e0c1c65f4971da", "sealed_file_identity": [66306, 33166473], "sealed_path": "data/onboarding/telegram-room-bootstrap-v1/ledger.json", "sealed_sha256": "f3a3dd5b7aec01200f7e577a0367365717aef5b96a528de01c0116a81316ee6d"}
journal_bootstrap_digest= f3a3dd5b7aec01200f7e577a0367365717aef5b96a528de01c0116a81316ee6d
current_bootstrap_digest= e3271199c1cb38476c44c55fe5318f5543a4152b4717033677e0c1c65f4971da
systemd state: active
activation artifacts: 0
task22 supplemental journal mtime: 2026-08-12 18:36:07.078034729 +0900


......                                                                   [100%]
6 passed in 0.37s


**Assessing activation gate and session key handling**

**Analyzing session ID and activation mismatch**

**Verifying AWAITING_ACTIVATION state binding logic**

**Identifying session status mutation issue**

**Confirming test environment integrity**

**Clarifying verification discrepancies in Task22 status**

**Verifying seal expiration and identity mismatch**

**Confirming test matrix success and manual bootstrap requirement**

**Identifying manual operator action for bootstrap**

**Identifying manual Telegram bootstrap requirement**

**Confirming evidence integrity and test outcomes**

**Verifying manual blocker due to expired session**

I read this as an implementation task - Task23 is blocked before activation, so I stopped without further live mutations.

- Task22 canonical code-only verifier passed; live journal remains `PUBLICATION_COMMITTED` with generations 27/28 and one callback.
- The supported `status` CLI unexpectedly expired the unbound bootstrap session: it is now `EXPIRED` (generation 8). This changed the bootstrap ledger from the Task22-sealed digest `f3a3...` to `e327...`.
- Cutover only accepts `AWAITING_ACTIVATION` or `ACTIVE`; no activation, check-in, generation job, delivery, or service restart was performed.
- Automated invalid/stale/duplicate/restart matrix passed: 6 targeted tests.
- Gateway remains active; activation/delivery artifacts remain absent.

**Manual blocker:** An operator must perform a fresh authenticated synthetic-customer DM bootstrap and rerun/reseal Task22 for that new session. The existing Task22 seal is bound to the expired session ID, and no supported recovery may restore or rebind it.