# DualCoach Task 21 preparation receipt

- Timestamp UTC: `2026-08-09T10:58:51Z`
- Verdict: **BLOCKED before rehearsal mutation**
- Pinned HEAD: `0dc0f228dcec8fa54430882daf72e901d6a89f79`
- Candidate: 92 paths, `fd358d90530eb803c5ad8b5868d83cb378ab15699d24d6eb9d01fc688ff42d80` (fresh raw-byte recomputation matched).
- Redacted pre-mutation snapshot SHA-256: `32c1598a3ca1cfd2778d0f418d88c6dace1b2efd6c5a8edbdaeaf92d9a7773fb`.

## Required pause failed closed

The only running unrelated writer is the `physique-coach` gateway. Before any DualCoach reset:

1. The public profile lifecycle command `hermes -p physique-coach gateway stop` returned `No gateway running for this profile`; its corresponding status surface resolved the default gateway rather than the named profile.
2. The explicit profile-scoped canonical lifecycle API `hermes_cli.profiles._stop_gateway_process(profile_dir)` also refused to signal: `No verified gateway process; refusing to signal by PID`.
3. The profile PID and lock authority records are legacy/incomplete: they contain only `pid`, `kind`, `argv`, and `start_time`, without the current required boot and canonical-home identity fields. The runtime lock remains active and the process remains live. The fail-closed refusal is therefore correct.

No raw signal, PID kill, lock deletion, metadata edit, reset, or alternate stop path was used.

## Non-touch receipts

| Scope | Pre snapshot | Post check | Result |
|---|---|---|---|
| DualCoach test profile | 2,477 files; `67e8f62e065c0c8d3e6b90d9cd297a585a89b52d73eb7a914fa93a3487de7358` | same | byte-identical |
| Physique profile | 37,773 files; `d7a753f889d9de736bd1f1da271de6b708fa6229e4be735b2a3e3a04f7a2ea67` | 37,776 files; `a49867ad7eaab3b74f2d2fbc96c6ffb394961d42789b7d3c3945bf373fc28e1f` | active writer continued; not touched by Task 21 |
| Default profile core, excluding named profiles | 79,925 files; `6ad621b6b1c0755c17a6712d489f687daf797c6c19aefcd5a7ee04a2562cdf1d` | 79,925 files; `cf76a3ac0275fe2bc9fd01d0909c4b678106c657dacdf1fd912d089aaf9aee86` | concurrent external change; not touched by Task 21 |
| DualCoach runtime metadata | SHA-256 `c9d6ac3ee206ef8d329dae79d4c1d5e79764fdd01c703ba71872f0cb47112e72` | same | unchanged; still stale `running` state with absent process |
| Default runtime metadata | SHA-256 `516153a5b3e5dccdad58ee5e6d2b2f0cc11d6ddd58b1d235259218a193515b48` | same | unchanged; still stale `running` state with absent process |

The candidate digest remained matched. The broad worktree has 970 status entries, SHA-256 `a896a7ad3964f757de7bc802417099695808f9bbfe055494a0864e0b69bea55f`; this differs from the 120-entry manifest snapshot due unrelated worktree activity. No candidate or manifest bytes were changed.

## Actions not performed

No DualCoach registry/bootstrap/draft/generation/delivery/publication/notice state was reset. No runtime metadata was reconciled. No Telegram updates were consumed, no membership was queried, no topology was changed, and no onboarding, activation, generation, or delivery was initiated. No raw customer data, Telegram IDs, tokens, or account details were retained.

## Exact unblock

The profile owner must restore canonical lifecycle authority for the currently running `physique-coach` gateway (a current PID/lock identity record accepted by the profile-scoped `gateway stop` API), then stop it through that canonical command and prove its lock is inactive. After that, Task 21 may retake the profile snapshots and perform the authorized DualCoach-only reset and topology preflight.

## Resumed authorization revalidation (2026-08-09T11:29:33Z)

- Verdict: **BLOCKED before any DualCoach mutation**. This current result supersedes the earlier `physique-coach` writer condition: its service is now inactive, its runtime is `stopped`, and its last shutdown reason is `operator_stop`.
- Rebaselined candidate: 92 paths at pinned HEAD `0dc0f228dcec8fa54430882daf72e901d6a89f79`; digest `20e2a680854ba83f5aab68a94b6ed4785ff4995d9c2fb8c5da658590b0b8229a`; manifest SHA-256 `2400609d52109203c03e351e57ead168416c2247542d49ca781f938d2835f0ee`. The five release-decision paths remain outside the candidate.
- Candidate gates: `tests/hermes_cli/test_gateway.py` **46 passed**; `ruff`, `py_compile`, and `git diff --check` passed. The wider service-lifecycle batch was `347 passed, 1 pre-existing failure`: the optional Discord platform cannot load in this Python 3.12 environment.
- Fresh redacted pre-mutation snapshot SHA-256: `b6b26eb84683c14ff6e69726d655150dfc2cad3157aab0246a7e59a7b3a129b3`; its prior fresh snapshot SHA-256 is `6d884c2347ea76aad865860187d086ff15d040788072557b2d9741ce8fdf6097`.

### Current canonical runtime result

- The default systemd service is active. Its metadata was not reconciled or changed.
- `dualcoachtest` has no PID authority file and no verified canonical process handle, but its runtime lock probe is **active**. Its legacy lock record lacks current boot/home identity fields; the current-UID `0664` authority file is rejected by the canonical authority opener with `OSError`. The failed user service does not provide a canonical authority to clear this lock.
- Because the active/invalid target lock is an explicit fail-closed condition, canonical status does **not** prove the target runtime is safe to reconcile. No direct metadata edit, permission change, lock deletion, signal, or alternate process action was used.

### Non-touch and cleanup

- The target tree remains exactly `2,477` files, SHA-256 `67e8f62e065c0c8d3e6b90d9cd297a585a89b52d73eb7a914fa93a3487de7358`; it matches the fresh pre-mutation snapshot. Config, auth, dedicated bot configuration, registry, bootstrap, draft, generation, delivery, publication, journal, and activation-notice hashes also match that snapshot.
- No reset, Telegram update inspection, membership query/change, topology rebind, invitation, onboarding, activation, generation, or delivery was performed. The worktree remains independently dirty with 970 all-files status entries, SHA-256 `a896a7ad3964f757de7bc802417099695808f9bbfe055494a0864e0b69bea55f`; no unrelated profile was mutated.
- Task-owned redacted `/tmp` snapshots are removed after this receipt.

### Exact unblock

The `dualcoachtest` lifecycle owner must use the approved lifecycle-recovery path to replace the malformed runtime authority state without manual lock deletion or metadata edits. It must then prove, through canonical status, a truthful stopped runtime with no verified target process and an **inactive** target runtime lock. Only then can Task 21 reconcile target metadata, perform the authorized DualCoach-only reset, and inspect the dedicated bot topology.

## Authorized resume revalidation (2026-08-09T11:37:35Z)

- Verdict: **BLOCKED before DualCoach state or Telegram mutation**. The lifecycle prerequisite is now satisfied: `dualcoachtest` has no PID authority file, no runtime lock, no verified process handle, a truthful `stopped` runtime state, and an inactive/dead dedicated service. `physique-coach` is likewise stopped. The default system service remains active by user instruction and was not stopped or changed.
- Candidate: pinned HEAD `0dc0f228dcec8fa54430882daf72e901d6a89f79`; 92-path digest `20e2a680854ba83f5aab68a94b6ed4785ff4995d9c2fb8c5da658590b0b8229a`, matching the refreshed manifest.
- Fresh redacted scoped-before snapshot SHA-256: `768ca4afde170edb24fb3b07e70b59f459b54dcb6bfacd95e72939b6eef8d3cd`.

### Authorized target baseline observed

- Registry: 7 customers, 0 enabled.
- Bootstrap: 1 session in `ACTIVE` state.
- Owner-action ledgers: 9 drafts, 2 completed generations, 2 `sent_audited` deliveries, and 13 requests.
- Rehearsal/activation receipts: 4 completion notices, 12 activation-audit rows, 2 projection rows, and 885 persisted operator-session rows. The gateway publication outbox is absent/empty.

### Canonical reset boundary blocks the destructive baseline

The supported product interfaces do not contain a destructive rehearsal-reset transaction for these records:

- customer administration can only disable an existing customer; it cannot remove a customer or its canonical history;
- the bootstrap store can cancel the active session, but deliberately retains its durable ledger row;
- onboarding `purge-incomplete` removes only transient onboarding artifacts;
- activation notices, publication receipts, adaptive/operator sessions, and draft/generation/delivery ledgers are append-only/reconcile-only and expose no clear/reset operation.

Consequently, no supported operation can produce the required empty draft, generation, delivery, notice, receipt, and rehearsal baseline. Manually truncating/deleting or replacing those files would violate the runbook's append-only and no-direct-metadata rules. I therefore did not perform a partial reset, consume dedicated-bot updates, or make topology/membership changes.

### Scoped non-touch and verification

- The DualCoach target is byte-identical to the scoped-before snapshot, including auth/config, registry, runtime, customer, bootstrap, owner-action, and receipt state.
- Protected default and `physique-coach` config, registry, runtime-authority, customer, and delivery scopes are byte-identical before/after. No global profile aggregate was used.
- Default log scope changed from SHA-256 `594899ceccb166f84b4e783473afca26a989bb5cf3aada5422e6ab87058e5399` to `a1eeae941f4647d1b80fc2b5cceaec5f3f23fc422ea2c135186b7c018e671804`; this is attributable to the still-running default gateway and excluded from the protected non-touch scope. `physique-coach` log scope was unchanged.
- Gateway rehearsal tests: **101 passed**. Profile customer-admin/onboarding tests: **98 passed**. Corresponding Ruff, compile, and compileall checks passed.

### Exact unblock

A release owner must provide an approved, profile-scoped cleanup transaction that can atomically archive or otherwise preserve the required immutable evidence while establishing the explicitly authorized empty rehearsal baseline. It must cover bootstrap, drafts, generations, deliveries, publication, notices, operator sessions, and the listed receipts under the relevant canonical locks. This is a product/lifecycle interface gap, not a Telegram marker problem; no human Telegram action can safely substitute for it. The task-owned scoped snapshot is removed after this receipt.

## Authorized reset execution and read-only preflight (2026-08-09)

- Verdict: **COMPLETED through the Task 21 boundary; stopped before Task 22.**
- The approved profile-only command was executed once with the required profile and empty-baseline confirmations. It created archive ID `9b1289330363d155ec14855690d6997c`, archived **12** named authorities, and returned digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`.
- The archive remains private under the dedicated profile. A fresh read-only `verify-archive` run recomputed the same digest and reported `valid: true`, with 12 archived scopes. No archive payload, customer content, IDs, or credentials are recorded here.

### Empty baseline and protected scope receipts

- The transaction's final canonical baseline check passed: registry customers are `[]`; customer-state is empty; bootstrap sessions are empty; publication and emergency-publication records are empty; activation notices are empty; the baseline service state is not paused; and the seven receipt/scheduled-delivery authorities are absent.
- Fresh independent checks found zero registry customers, zero customer-state entries, the archive present, the reset control directory absent, and the protected `config.yaml`, `.env`, and `auth.json` still present. The external `checkin_cli` runtime loader returned zero customers.
- The reset capability is hard-coded to `dualcoachtest`; it did not start or stop a gateway and made no call against the default or any unrelated profile. No raw PID signal, lock deletion, direct metadata edit, `git clean`, onboarding, activation, generation, delivery, invitation, or customer mutation occurred.

### Dedicated-bot pending-update/topology preflight

- Read-only Telegram identity inspection proved that the configured profile token belongs to the configured dedicated bot. The token and all raw Telegram response fields were discarded and are not retained in evidence.
- `getWebhookInfo` reported no configured webhook and zero pending updates. A single `getUpdates` request used no `offset`, returned zero updates, and therefore did not acknowledge or consume an update. No membership query or mutation was made.
- There are no pending topology markers and no membership proof. No group, topic, routing, or bot-topology construction was attempted. This is the required stop point before Task 22/live rehearsal.

### Current candidate and verification

- The candidate manifest was refreshed at `2026-08-09T21:12:06+09:00`: **96** exact raw-byte paths, digest `b852e5bd4c1eed12fe4bd753f47a4c576307b2a6ec92730167d9ef2b40a889b7`. An independent recomputation matched every recorded file hash and the digest.
- `tests/gateway/test_rehearsal_reset.py`: **5 passed**. Ruff, `ty` on the reset modules, `py_compile`, and scoped `git diff --check` passed. The direct wrapper and installed entry point both passed `--help`; wheel and sdist builds passed and the wheel contained both reset modules plus the declared entry point.
- Task-created `build/`, reset distribution artifacts, reset bytecode, temporary wheel output, and the redacted preflight temporary log were removed explicitly. No unrelated files were cleaned.

## Archive-tree verification hardening (2026-08-09)

- **Failing-first receipt:** before the verifier change, the expanded reset test file produced **7 failed, 8 passed**. It demonstrated that an unexpected payload regular file, directory, symlink, root entry, FIFO, path-escape symlink, and a wrong-mode payload directory were accepted. Missing entries, hard-link aliases, and wrong-mode manifest files were already rejected by the prior per-scope checks.
- The verifier now derives the exact root/payload/manifest/receipt/scope-root layout from the authenticated manifest and receipt, then walks it through descriptor-anchored, no-follow directory handles. It rejects unexpected and missing structural entries, aliases/hard links, symlinks and escape names, special files, owner/mode violations, and scope type/count inconsistencies. Scope descendants receive the same no-follow private-mode walk before their existing digest comparison.
- Archive creation now makes each staging ancestor private rather than allowing `Path.mkdir(parents=True)` to retain a process-umask mode. The on-disk archive schema, receipt, payload paths, and digest format are unchanged; there is no compatibility fallback.
- **Passing receipt:** `tests/gateway/test_rehearsal_reset.py` passed **15/15** after the change. Ruff, scoped `ty`, `compileall`, and scoped `git diff --check` passed.

### Existing archive manual QA and non-touch result

- Read-only CLI QA did **not** report the historical live archive as valid under the new strict contract: `verify-archive` returned `valid: false` (exit 1). Its receipt still matches the archived manifest digest, with 12 scopes and no non-private archive files; the sole defect is the pre-existing required ancestor `payload/data/onboarding` at mode `0775` rather than private `0700`.
- No compatibility exception was added and the immutable archive was not chmodded, replaced, deleted, or reset. Therefore the requested claim that this already-created archive remains valid cannot be made without an explicitly authorized archive-metadata migration; no destructive reset of `dualcoachtest` was rerun. The profile has no reset control directory, and no Telegram, topology, Task 22, onboarding, activation, generation, or delivery action occurred.
- The candidate manifest was refreshed at `2026-08-09T21:32:10+09:00`: **96** exact raw-byte paths, digest `1a2f87b1e4ef05cb69a6dbbe2f77398c1f5ac60e67165a3a36178d15a7f602c4`.

## Authorized archive-mode migration and resumed live preflight (2026-08-09)

- The user explicitly approved the one historical archive metadata correction. The supported, profile-scoped CLI transaction required matching profile/archive confirmations and an explicit private-mode confirmation; its negative missing-confirmation check rejected without mutation.
- Migration receipt: archive `9b1289330363d155ec14855690d6997c`; approved ancestor mode `0775 -> 0700`; archive digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`; manifest SHA-256 `d53baab0f8c4b11096829ee88981b52238de6616bb1a1084517c0660082f1f6f`; receipt SHA-256 `dcad1f4f958c54d1ca55e5e4b1f8d7ba5a633f44a807567f055b01ffa362ad6c`.
- The archive has 220 regular single-link files. Its aggregate no-follow file-byte SHA-256 remained `850e858e1dc19e8974d9b054c1a5feea25b3af479dfcf8fa67027c55572b1db9` before and after migration. Strict no-follow completeness and payload verification now report `valid: true`, 12 scopes, and the same archive digest. Both reset and migration control directories are absent.
- The empty rehearsal baseline remains intact: zero registry and external runtime customers, zero customer-state entries, and all seven absent receipt/scheduled-delivery authorities remain absent. Dedicated target services are inactive and no target PID/lock authority exists.

### Repeated dedicated-bot topology preflight

- The dedicated identity matches configuration. Exactly one pending `/start`, `/pilot_owner`, and `/pilot_trainer` marker was found; all three roles are distinct humans, the customer marker is a private DM, and owner/trainer markers are in the same private forum supergroup with topics enabled.
- The customer is absent from that group; owner and trainer are present. The bot is an administrator with `can_manage_chat` and `can_manage_topics`. No webhook is configured.
- A no-offset, no-`allowed_updates` `getUpdates` returned 10 updates; pending count remained 10 before and after. No update was acknowledged or consumed. No local target consumer exists. Telegram exposes no read-only inventory for an intermittent remote consumer, so that external condition remains observationally bounded rather than globally provable.

### Verification and bounded DoneClaim

- `tests/gateway/test_rehearsal_reset.py`: **17 passed**. Ruff, scoped `ty`, `compileall`, scoped `git diff --check`, CLI help, and negative confirmation checks passed.
- The candidate manifest was refreshed at `2026-08-09T22:19:28+09:00`: **96** exact raw-byte paths, digest `8aacdbec22a8fe3f7e4922d1b16d7fc9fd793edb3fda5a9d11daa1a60382bffb`.
- **Bounded Task 21 DoneClaim:** the authorized empty baseline, strict archive integrity, approved historical permission correction, stopped target runtime, and read-only dedicated-bot topology preflight are complete. No Task 22 action is authorized by this claim. No Telegram send/invite/membership mutation, onboarding, activation, generation, delivery, other-profile change, or archive file-byte change occurred.

## Reopened HIGH: fail-closed data-authority inventory (2026-08-09)

- **Failing-first receipt:** the focused reset suite reported **9 failed, 16 passed** before implementation. The failures proved that direct `data/` spillover, direct and nested `data/onboarding/` sidecars, a symlink escape, an external hard-link alias, a FIFO, a non-private onboarding ancestor, and lock-authority rollback were not covered; the new 16-scope archive expectation also failed against the old 12-scope contract.
- Reset archive schema v2 has exactly 16 canonical DualCoach scopes. It adds the four durable top-level lock authorities. A successful new reset copies every present scope to the authenticated manifest/payload, then rebuilds those lock files as empty private baseline files. Historical schema v1 remains a separate exact, strict verifier contract; it is not a permissive fallback.
- Before any transaction state is created, reset now inventories every direct `data/` sibling and every direct `data/onboarding/` sibling. Only canonical DualCoach authorities, reset operational roots, and these named non-DualCoach roots are accepted: `global`, `migrations`, and the five named Gate-D/virtual checklist files. Any other file, directory, nested sidecar, link, hard-link alias, FIFO, or path escape fails closed. Named preserved roots are not copied, removed, mode-normalized, or otherwise changed.
- Deterministic tests cover `data/spillover.txt`, `data/onboarding/sidecar.txt`, nested sidecars, symlink escapes, hard-link aliases, FIFOs, ancestor mode rejection, named-root byte preservation, manifest coverage of removed lock authorities, and interrupted-transaction rollback. `tests/gateway/test_rehearsal_reset.py:273` now uses a `TypeGuard[dict[str, object]]`; scoped strict `ty` reports zero diagnostics without `Any`, casts, or suppressions.
- **Passing receipt:** focused reset suite **25 passed, 0 failed**; Ruff, scoped strict `ty`, `compileall`, and scoped `git diff --check` passed.

### Read-only live survivor inventory and stop condition

- The new classifier was invoked read-only against `dualcoachtest` before any reset/archive transaction. It rejected exactly one unclassified direct sibling: `data/data` (a private directory containing one private empty lock-named descendant). This location is not a canonical DualCoach authority and is not a named preserved root; it cannot be safely reclassified or silently retained by a successful reset.
- Current direct-entry classes: 7 canonical DualCoach roots (customers, onboarding, owner actions, and four empty private lock files), 7 named preserved non-DualCoach roots, 1 reset archive root, and 1 unclassified root. The four canonical lock files and the duplicate nested lock are all regular `0600`, single-link, zero-byte files (SHA-256 `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`).
- No supplemental archive transaction was run: the only unclassified survivor is outside the authorized canonical scope. Its provenance cannot be proven from its path, so moving it would violate the fail-closed rule. The zero-byte canonical lock files are regenerated baseline artifacts, not evidence that unarchived customer authority survived the historical v1 reset. No destructive reset was rerun.
- Historical archive `9b1289330363d155ec14855690d6997c` still strictly verifies with its v1 12-scope contract and digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`. Its manifest SHA-256 remains `d53baab0f8c4b11096829ee88981b52238de6616bb1a1084517c0660082f1f6f`; receipt SHA-256 remains `dcad1f4f958c54d1ca55e5e4b1f8d7ba5a633f44a807567f055b01ffa362ad6c`; no reset control directory exists.
- The candidate manifest was refreshed at `2026-08-09T22:45:23+09:00`: **96** exact raw-byte paths, digest `fa3814cc3fc484a1a9eb3b358a120cf313c60e2d7c224e8ebac790fb0bf3b365`.
- **Task 21 status:** reopened hardening is implemented and verified; live reset success is intentionally blocked by the unclassified `data/data` survivor. The precise next action requires separate authorization to classify/remove or preserve that root. Task 22 remains out of scope. No Telegram request was made during this reopened work.

## Authorized supplemental quarantine of the historical duplicate (2026-08-09)

- The user explicitly authorized one operation for `dualcoachtest/data/data` only. A supported durable `quarantine-duplicate-data-root` transaction now has a separate exact one-scope authenticated archive schema and journal. It requires profile confirmation and a duplicate-root confirmation; its missing-confirmation CLI path rejected without mutation. It never calls Telegram or a reset.
- **Failing-first receipt:** before implementation, the focused suite stopped at collection with `ImportError: cannot import name 'quarantine_rehearsal_duplicate_data_root'`; strict `ty` reported the matching unresolved import. **Passing receipt:** `tests/gateway/test_rehearsal_reset.py` reported **30 passed, 0 failed**. Ruff, strict `ty`, `compileall`, and scoped `git diff --check` passed.
- Immediately before the transaction, no-follow source preconditions matched exactly: private current-user directory, one private single-link zero-byte lock file, SHA-256 `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`, source-tree digest `b8914a492802f78fdafd49bd059339765b20b9c4f3a890a106dc81dca858ebe2`, one file, and one directory. Target services were inactive; no gateway authority or pre-existing supplemental control directory existed.
- Confirmed CLI receipt: archive ID `245f9c72d6cc6ab22a8d9d9830bbd90f`; archive digest `9f528bfdcf09c861abdc48bfac08a27528f83566e16dd829bb257e3a2d120bcb`; one archived scope; status `supplemental_quarantine_committed`. The supplemental manifest SHA-256 is `c8ea4cbc062209ec4900592a5584f21c414a785c8a3e5fa620b45a94e2a8aba6`; receipt SHA-256 is `a36ad248f980b3ed2d2f6033dbb16d8e31b649169da2339f7502e981ac32ad67`.
- The new archive strictly verifies with one scope and contains exactly the approved private empty lock payload. The source is absent, no supplemental control directory remains, and the new data-authority classifier accepts the live profile. A no-follow, byte-and-metadata snapshot of all 29 non-target `data` entries remained exactly `5e2a2dda1c194c1a2d440ad9fdc3e3b6362705fc201596730f5f4377fa9812c1` before and after. The historical 12-scope archive still strictly verifies with its unchanged digest and authenticated metadata hashes.

### Final read-only Telegram preflight after quarantine

- The dedicated bot identity, exactly one each `/start`, `/pilot_owner`, and `/pilot_trainer` marker, distinct human identities, customer private DM, private forum-enabled staff supergroup, customer absence, owner/trainer membership, and bot administrator/topic-management permissions all pass.
- No webhook is configured. A no-offset, no-`allowed_updates` `getUpdates` returned 10 updates; pending count remained 10 before and after. No update was consumed or acknowledged. No local target consumer exists; intermittent remote-consumer absence remains observationally bounded by Telegram's read-only API.

### Final Task 21 receipt

- The candidate manifest was refreshed at `2026-08-09T23:04:17+09:00`: **96** exact raw-byte paths, digest `a02c6e6f2206e5a8dba51c3d89dbe138337fd200894d6cb81c703ae0a94c9c31`.
- **Task 21 DoneClaim:** the historical duplicate is durably quarantined in a strict authenticated archive; the live data authority surface is classified and accepted; strict archival verification, empty baseline checks, and the final read-only Telegram topology preflight pass. No other profile was targeted, and no plan checkbox or ledger was edited. Task 22 remains out of scope.

## Control-orphan recovery hardening (2026-08-09)

- Reset and supplemental-quarantine controls now scan before taking a cleanup-capable control lock. A non-empty control or `transactions/` tree without a valid journal fails closed with `recovery-required`; it is neither ignored nor removed. Malformed journals continue to fail closed as invalid. The same checks apply to both regular operation and explicit recovery; valid reset and supplemental applying journals still recover normally.
- **Failing-first receipt:** focused tests reported **2 failed, 33 passed** because orphan reset and supplemental controls without journals were accepted. **Passing receipt:** **35 passed, 0 failed** after the fix. Coverage includes reset and supplemental orphan trees, malformed journals, valid reset recovery, valid interrupted supplemental recovery, and existing archive/reset behavior.
- Ruff, strict `ty`, `compileall`, and scoped `git diff --check` passed. No live reset, quarantine, recovery, archive mutation, or Telegram request was performed for this hardening pass.
- Read-only live validation still accepts the authority surface; `data/data` remains absent; reset, supplemental, and archive-mode control directories are absent. Historical archive `9b1289330363d155ec14855690d6997c` remains strict-valid with digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`; supplemental archive `245f9c72d6cc6ab22a8d9d9830bbd90f` remains strict-valid with digest `9f528bfdcf09c861abdc48bfac08a27528f83566e16dd829bb257e3a2d120bcb`. Their manifest/receipt SHA-256 values remain unchanged.
- The candidate manifest was refreshed at `2026-08-09T23:26:19+09:00`: **96** exact raw-byte paths, digest `bb55a82938f713269d6a23e3e04e7f6a8ffae4325f3de9795e901924ca88bd0d`.
- **Task 21 DoneClaim remains bounded:** recovery handling is now fail-closed for orphan control state. No plan checkbox or ledger was edited; Telegram pending updates were not queried or consumed; Task 22 remains out of scope.

## Lock-only crash recovery and preserved-root no-follow hardening (2026-08-09)

- **HIGH control recovery:** the sole acceptable no-journal residue is now exactly one current-user `reset.lock`: a regular, single-link, zero-byte `0600` file under a private control directory. Recovery opens it with `O_NOFOLLOW`, rechecks inode and metadata, acquires a non-blocking exclusive flock, rescans the control directory while holding that flock, then unlinks and fsyncs the empty control. An active holder, symlink, hardlink, wrong mode, unexpected content, or any additional control/transaction entry fails `recovery-required` and is retained.
- Deterministic abrupt-exit failpoints cover both reset and supplemental quarantine immediately before journal writing and immediately after journal cleanup. Each leaves only the expected lock; explicit recovery cleans it without rollback. Valid interrupted applying-journal recovery remains covered separately.
- **HIGH preserved authority scan:** before a reset transaction, and before supplemental quarantine transaction setup, every named preserved root is descriptor-anchored and recursively no-follow scanned. This includes `data/global`, `data/migrations`, and the five preserved checklist files. The scan rejects path escapes/symlinks, hardlinks, FIFO/device/socket entries, ownership changes, and unauthorized modes; it accepts the legitimate current-user `0775` legacy roots, private descendants, and private regular checklist files. Tests cover unsafe global/migrations descendants and supplemental pre-transaction rejection.
- **Failing-first receipt:** focused tests reported **11 failed, 45 passed** before these HIGH fixes. **Passing receipt:** `PYTHONDONTWRITEBYTECODE=1 uv run python -m pytest -q -W error tests/gateway/test_rehearsal_reset.py` reported **57 passed, 0 failed**. Ruff, strict `ty`, `compileall`, and scoped `git diff --check` passed.
- Read-only live validation accepted the new preserved-root scan; `data/data` and all control artifacts remain absent. Historical archive `9b1289330363d155ec14855690d6997c` remains strict-valid with digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`; supplemental archive `245f9c72d6cc6ab22a8d9d9830bbd90f` remains strict-valid with digest `9f528bfdcf09c861abdc48bfac08a27528f83566e16dd829bb257e3a2d120bcb`. Both manifest/receipt hashes remain unchanged.
- The candidate manifest was refreshed at `2026-08-09T23:47:44+09:00`: **96** exact raw-byte paths, digest `581389f0367cbd330486466f7db172c259ab4faf077fafb924dd095bae89bfc8`, manifest SHA-256 `01d8abb818a5ca72a1a46d85d8086927dc6b5339b86bce5ff88d5c706bab9430`.
- **Task 21 DoneClaim remains bounded:** no live reset, quarantine, recovery, archive mutation, or Telegram request was performed; plan and ledger remain untouched; Task 22 remains out of scope.

## Verify-archive profile-ancestry hardening (2026-08-10)

- **Confirmed gap and minimal fix:** the read-only `verify-archive` command previously constructed an archive path and called the generic archive verifier without authenticating the configured profile, validating the profile data-authority surface, or checking the profile-to-archive ancestry. `verify_rehearsal_profile_archive` now performs those three checks; `_authorized_archive_path` validates the archive ID, private archive parent, and no-symlink ancestry through the authenticated profile root. The generic verifier remains the strict archive-content primitive.
- **Failing-first receipt:** `PYTHONDONTWRITEBYTECODE=1 uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py -k 'verify_archive_rejects_symlinked_profile_data_and_archive_ancestry or verify_archive_keeps_historical_archive_schema_valid or verify_archive_keeps_supplemental_archive_schema_valid'` returned **3 failed, 2 passed** before the fix. The profile, data, and archive-parent redirect cases each returned a false `valid: true` command result while the generic archive verifier read the redirected archive.
- **Passing receipt:** the same selected command returned **5 passed, 0 failed** after the fix. The full focused command `PYTHONDONTWRITEBYTECODE=1 uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py` returned **62 passed, 0 failed**. Direct regressions reject symlinked profile, `data`, and archive-parent ancestry with a redacted invalid result. Synthetic strict historical v1 (12 scopes) and supplemental-quarantine v1 (1 scope) archives still verify through the profile-bound command.
- **Existing archive read-only proof:** the profile-bound CLI returned `valid: true` for the existing strict historical archive (12 scopes, digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`) and supplemental archive (1 scope, digest `9f528bfdcf09c861abdc48bfac08a27528f83566e16dd829bb257e3a2d120bcb`). These commands only read the target profile and archives; neither archive nor profile bytes changed.
- **Prior HIGH recheck:** the 62-test focused result retains abrupt lock-only reset and supplemental recovery coverage before journal creation and after cleanup, plus recursive descriptor-anchored preserved-root checks for link, hard-link, FIFO, mode, and owner failures. These suites passed with warnings treated as errors.
- **Static gates:** Ruff, strict scoped `ty` with warnings as errors, `compileall`, and `git diff --check` all passed on `gateway/platforms/rehearsal_reset.py` and `tests/gateway/test_rehearsal_reset.py`.
- **Frozen candidate:** 96 exact raw-byte paths; digest `d2c37b864e5e5f630f31184ad520d2fafc8a43f2eed66a9de96fe2170e4eba7e`; status snapshot 131 entries, SHA-256 `89714c44258925cadf4612395cac60cb495871e02a89306f41e1f34bf529428c`. Independent manifest recomputation matched every listed file hash and digest. Artifact SHA-256: reset module `ce70c2002040874e87a388f0c55c4c9b927fc4862a7aa0eb85d2c1f26a934e62`; reset tests `2a9c69417cf94922314e0a78bf169441e0ee76c06a50b5c2a627b0803958e6c7`; manifest JSON `faf3e6477f09330b354e8930117edb9030ff0c87acf7016c29863b724efb79e5`; manifest Markdown `c86ae6180b750d20d94f317461141136ba1301085306d10fdec0a1f5fb53cd4d`.
- **Non-touch, redaction, and cleanup:** all tests used temporary profiles only. No live profile, archive, control, Telegram surface, update queue, customer state, delivery, or other profile was accessed or mutated. The plan, ledger, and checkbox remain untouched. Task-created compile bytecode was removed; no temporary test profiles or processes remain. This evidence records no customer content, account identifier, token, or credential.

## Final Task 21 safety closure (2026-08-10T00:43:05+09:00)

### Reused behavior, gap, and minimal delta

- Reused the existing profile authorization, exact authority inventory, no-follow tree checks, archive-first journals, strict archive verification, rollback/recovery, empty-baseline proof, and dedicated read-only Telegram preflight.
- Closed only two acceptance gaps: destructive path operations did not retain the authenticated profile/data identity across mutation boundaries, and the live-process gate filtered a matching canonical `HERMES_HOME` through gateway-like argv text.
- Added a captured profile/data inode identity guard, component-by-component descriptor-relative directory traversal, descriptor-relative rename/publication, guarded archive-mode chmod, and fail-closed recovery phases (`root_replaced`, `data_replaced`, `ancestor_replaced`). Recovery-required identity failures do not attempt rollback through rebound path strings; after the authorized path is restored, the durable journal rolls back normally.
- `_live_process_uses_profile()` now blocks immediately on a canonical matching `HERMES_HOME`, independent of argv.
- Rejected from scope: reset schema changes, new archive formats, generic filesystem abstractions, process-command heuristics, customer activation, onboarding, coaching send, Task 22 work, and unrelated refactors.

### Failing-first and automated gates

- Existing unchanged baseline: `PYTHONDONTWRITEBYTECODE=1 timeout 120s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py` -> **62 passed**.
- Failing-first command: `PYTHONDONTWRITEBYTECODE=1 timeout 60s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py -k 'profile_ancestor_identity_change or matching_profile_environment'` -> **2 failed**: missing `RehearsalResetRecoveryRequired`; unrelated-argv process was not blocked (the first test's early attribute failure and unclosed-pipe warning were also captured and then corrected in the deterministic test harness).
- Final focused failpoint command: `PYTHONDONTWRITEBYTECODE=1 timeout 90s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py -k 'identity_change or symlink_swap'` -> **3 passed, 63 deselected**. Reset profile rename/symlink swap and supplemental data rename/symlink swap both fail closed, then recover through their durable journals after path restoration; archive-parent symlink swap fails before chmod.
- Final full command: `PYTHONDONTWRITEBYTECODE=1 timeout 180s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py` -> **66 passed in 0.77s**.
- `uv run ruff check gateway/platforms/rehearsal_reset.py tests/gateway/test_rehearsal_reset.py` -> pass.
- `uv run ty check --output-format concise --color never gateway/platforms/rehearsal_reset.py tests/gateway/test_rehearsal_reset.py` -> pass, zero diagnostics.
- `PYTHONPYCACHEPREFIX=/tmp/dualcoach-task21-pycache python -m compileall -q gateway/platforms/rehearsal_reset.py tests/gateway/test_rehearsal_reset.py` -> pass.
- `git diff --check -- gateway/platforms/rehearsal_reset.py tests/gateway/test_rehearsal_reset.py` -> pass.
- LSP diagnostics were attempted for both changed Python files; the tool rejected both absolute product-repository paths because they are outside the task workspace root. Strict `ty`, Ruff, compileall, and execution gates cover both files without suppression.

### Real-surface and archive QA

- Provider command: `HERMES_HOME=/home/cube/.hermes/profiles/dualcoachtest timeout 30s uv run python - ... preflight_nutrition_generation_provider()` -> `provider_auth_preflight: true`.
- Historical archive command: `timeout 30s uv run python -m gateway.platforms.rehearsal_reset verify-archive --archive-id 9b1289330363d155ec14855690d6997c` -> valid, 12 scopes, digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`.
- Supplemental archive command: `timeout 30s uv run python -m gateway.platforms.rehearsal_reset verify-archive --archive-id 245f9c72d6cc6ab22a8d9d9830bbd90f` -> valid, 1 scope, digest `9f528bfdcf09c861abdc48bfac08a27528f83566e16dd829bb257e3a2d120bcb`.
- Local baseline independently remained: 0 registry customers; absent `data/data`, all three control directories, gateway PID, and gateway lock; all four baseline lock placeholders were zero-byte and accepted a nonblocking exclusive flock; all three dedicated systemd unit names were inactive; no `dualcoachtest` process was present.
- Literal Telegram probe contract: source only `/home/cube/.hermes/profiles/dualcoachtest/.env`, call `getMe`, `getWebhookInfo`, exactly one `getUpdates?timeout=0` with no `offset` and no `allowed_updates`, a second `getWebhookInfo`, then read-only `getChat`/`getChatMember` for marker-derived dedicated identities. Result: pending `10 -> 10`, 10 returned updates, no webhook, exactly one `/start`, `/pilot_owner`, and `/pilot_trainer`, distinct synthetic humans, customer private DM, private forum staff supergroup, customer absent, owner/trainer present, bot administrator with chat/topic management. Redacted temporary receipt SHA-256: `6f35171ebaeb7d639a464190afc53f44c6f9b895fa4d50761f37b9144786f030` (removed after transcription).
- No Telegram update was acknowledged, no offset advanced, no membership changed, no message sent, no gateway started, no customer activated, and no coaching delivered.

### Candidate provenance and adversarial closure

- Final immutable candidate: **96 exact raw-byte paths**, digest `0992b76d2ad0300c856201c8e5ec58f9f3272ce50ea44af536541985f008049a` using UTF-8 byte-sorted `path + NUL + exact bytes + NUL` canonicalization.
- Candidate bytes: reset module SHA-256 `f42e17c6eefec734b2785aff954d8b1cb91e08f18163e6c3955fabae22460dca`; reset tests SHA-256 `741e77c888bc0bce5b59640468b6f9ba6f257c6f44d248dfae32b92ba8627af0`.
- Dirty-worktree snapshot remained exactly 131 entries, SHA-256 `89714c44258925cadf4612395cac60cb495871e02a89306f41e1f34bf529428c`; the two candidate paths were already untracked, so status membership did not change. No unrelated path was edited by this closure.
- Malformed/path boundary: existing traversal, symlink, hardlink, FIFO, mode, owner, and escape tests pass; new profile/data/archive ancestor swaps fail closed.
- Stale state: stale root/data/archive identities cannot redirect mutation; existing orphan, malformed journal, stale archive, and collision tests pass.
- Dirty worktree: stable status digest plus scoped file hashes prove unrelated changes remained untouched.
- Hung/long commands: every potentially long command used `timeout` (30-180s); none timed out. The process test uses a readiness line and stdin event, then bounded `wait(timeout=5)`, with no sleep or polling.
- Flaky tests: deterministic monkeypatch failpoints trigger exact destructive boundaries; one clean full run passed.
- Misleading success: CLI text was independently checked against archive digests, filesystem absence/counts, flocks, process list, systemd state, and Telegram pending before/after.
- Repeated interruptions: pre-existing abrupt-exit boundaries pass; new reset/supplemental rebinding tests restore the authorized path and prove journal recovery.
- Prompt injection: not applicable; no untrusted prose entered an LLM path.
- Customer delivery/exactly-once: not applicable to Task 21; customer remained disabled/absent and no delivery path was entered.
- Browser/device UI: not applicable; Task 21's authorized real surface is read-only Bot API/archive/profile preflight.

### Cleanup

- Removed `/tmp/dualcoach-task21-pycache` and `.omo/evidence/temp/dualcoach-task21-closure` after recording redacted hashes.
- No temporary credential file was created; token remained sourced in-process from the dedicated profile and was never printed or persisted.
- The unrelated-argv subprocess exited 0 and both pipes were explicitly closed; no Task 21 process, monitor, service, lock holder, or temporary profile remains.
- Plan checkbox, Boulder/todos, ledger, and Task 22 state remain untouched.

## Revised descriptor-lifetime closure (2026-08-10)

- Two independent verifier findings against candidate `0992b76d...` were accepted as valid. The prior identity check still released pathname authority before `_mkdir_private`, did not pin every traversed ancestor, and performed pathname chmod after descriptor-relative rename.
- **Failing-first receipt:** `PYTHONDONTWRITEBYTECODE=1 timeout 90s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py -k 'normal_directory_profile_replacement or normal_directory_archive_parent_rebind or post_rename_replacement'` returned **3 failed**. The normal replacement probe reached an external replacement profile, archive publication completed through a rebound normal directory, and pathname chmod changed the substituted post-rename file to `0600`.
- Added a separate normal-directory `data` rebind probe immediately between source-parent and destination-parent acquisition. It asserts the replacement directory remains empty and the operation raises `data_replaced`. This test was added while implementing the same failing class; the independent verifier receipt is the failing-first evidence for that exact data/archive normal-directory race.
- **Minimal repair:** active transactions now walk from a captured profile descriptor component by component with `O_NOFOLLOW`; compare every previously captured root, `data`, control, archive, transaction, and target ancestor inode; create missing directories with `mkdirat`; retain the exact target-parent descriptors across `renameat`, file creation, fsync, unlink, and rmdir; and recursively clean staging trees descriptor-relative. Existing archive-mode targets are pinned against the prevalidated target inode before journaling and revalidated before `fchmod`.
- Active private writes now create, `fchmod`, write, fsync, and rename the temporary file through one retained parent descriptor. The written file descriptor is permissioned before rename; there is no post-rename pathname chmod. The non-transactional fallback remains for generic standalone archive construction only.
- Reset locks and lock-only crash cleanup now open, validate, flock, unlink, fsync, and remove through captured control/data descriptors. Context identity is reset even when setup fails before yielding.
- **Focused pass:** `PYTHONDONTWRITEBYTECODE=1 timeout 120s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py -k 'normal_directory or post_rename_replacement or identity_change or symlink_swap or matching_profile_environment'` -> **8 passed, 62 deselected**.
- **Final full pass:** `PYTHONDONTWRITEBYTECODE=1 timeout 180s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py` -> **70 passed in 0.99s**.
- Ruff and strict `ty` on both changed Python files passed with zero diagnostics. Redirected-cache `compileall` and scoped `git diff --check` passed. LSP was attempted again and rejected both external product-repository paths as outside the task workspace root.
- Provider authentication preflight passed. Historical archive remains strict-valid with 12 scopes and digest `b430ef6325ed30408297707004b66d543dfd703331070f15517cd25b3a2c37ef`; supplemental archive remains strict-valid with 1 scope and digest `9f528bfdcf09c861abdc48bfac08a27528f83566e16dd829bb257e3a2d120bcb`.
- Independent live state remained: zero registry customers; no duplicate data root, reset/quarantine/migration control, PID, or gateway lock; all four baseline locks zero-byte and inactive; all dedicated service names inactive.
- Repeated read-only Telegram preflight used exactly one `getUpdates?timeout=0` with no offset or `allowed_updates`; pending remained `10 -> 10`. Dedicated identity, marker cardinality, distinct roles, private customer DM, customer-free forum staff group, memberships, and bot admin/topic permissions passed. Redacted temporary receipt SHA-256 remained `6f35171ebaeb7d639a464190afc53f44c6f9b895fa4d50761f37b9144786f030`.
- Revised immutable candidate: **96 paths**, raw-byte digest `582623f2b78c1e4acd6d4f2734bdc95bdb019976010ac2b7964d15fd19cea670`. Product SHA-256 `7e2f999631b17d3f1a5863b60880b107a0afb8f871e7356c8e9b44494642ba1f`; tests SHA-256 `08ffd4782e9af40f36c888655ee88a3717e9fd19890a2cef741a2ad18187a5c5`. Dirty-worktree status remains the same 131-entry digest `89714c44258925cadf4612395cac60cb495871e02a89306f41e1f34bf529428c`.
- No reset, live archive mutation, Telegram send, update acknowledgement, membership change, service action, customer activation, coaching delivery, Task 22 action, plan checkbox, ledger, Boulder, commit, or Git state mutation occurred.

## Active-store containment closure (2026-08-10)

- The verifier's remaining HIGH against candidate `582623f2...` was reproduced as a missing containment layer: descriptor-safe staging paths were handed back to path-mutating constructors, allowing `RoomBootstrapStore`, `CustomerServiceStateStore`, and `GatewayOnboardingPublicationOutbox` to reopen rebound profile/data paths. Baseline verification also instantiated those stores and `ActivationNoticeStore`, which can create or chmod lock/path state while ostensibly verifying.
- **Failing-first command:** `PYTHONDONTWRITEBYTECODE=1 timeout 90s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py -k 'replacement_store_boundary_rebind or bootstrap_ledger_substitution'` -> **4 failed**. Three boundaries lacked the deterministic failpoint argument, and the bootstrap ledger substitution was not observed because the path-based store used `os.replace` followed by pathname chmod.
- `_build_replacement_tree` now writes the exact empty bootstrap, service-state, and publication contracts directly through `_mkdir_private` and `_write_bytes_private`, both of which remain descriptor-anchored for the active transaction. It creates only: bootstrap private directory, empty private lock, digest-authenticated empty ledger; service-state private directory, empty private lock, digest-authenticated empty ledger; publication private directory, two empty private locks, a 32-byte private receipt key, and two canonical empty v2 ledgers.
- `_verify_empty_baseline` no longer instantiates any store. It reads every expected baseline file through `_read_transaction_private_bytes`, which opens the captured parent descriptor, no-follow stats and opens the exact file, compares the opened inode, enforces `0600`/single-link regular-file metadata, and reads through that descriptor. It verifies exact bootstrap/service/publication bytes, publication key length, absent activation notices, and the existing scope baseline contract without creating or chmodding anything.
- Deterministic `fault_before_baseline_store` boundaries named `bootstrap`, `service_state`, and `publication` replace live `data` with a fresh normal directory immediately before each baseline section. Every case raises `data_replaced`; each replacement directory remains empty.
- The bootstrap ledger post-rename substitution test replaces the published ledger through the retained parent descriptor. Baseline verification rejects it, while the substituted attacker file remains `0640`; no pathname chmod occurs.
- Removed redundant post-rename pathname chmod from `RoomBootstrapStore._write_unlocked` and `CustomerServiceStateStore._write`. Their temporary files were already `fchmod(0600)` before atomic replacement. Publication ledger writers already `fchmod` temporary files before replacement and had no post-rename pathname chmod; active reset no longer invokes publication path preparation or writers.

### Active-transaction callee audit

- `_create_archive`: destination creation/copy/write/fsync uses reset-owned anchored primitives; archive verification is read-only.
- `_build_replacement_tree`: only reset-owned `_mkdir_private`, `_write_bytes_private`, digest/JSON helpers, and `secrets.token_bytes`; no store constructor remains.
- `_verify_empty_baseline`: only reset-owned descriptor reads, exact byte/schema checks, directory inventory, and scope checks; no store constructor remains.
- `RoomBootstrapStore`: absent from active reset construction and verification; redundant post-rename chmod removed and 14 compatibility tests passed.
- `CustomerServiceStateStore`: absent from active reset construction and verification; redundant post-rename chmod removed and 3 selected compatibility tests passed.
- `GatewayOnboardingPublicationOutbox`: absent from active reset construction and verification; publication baseline is written/read through reset-owned descriptors; its writer already permissions the temporary inode before replacement.
- `ActivationNoticeStore`: absent from baseline verification; activation notice absence and baseline lock are checked without constructor/lock mutation.
- Transaction journals, locks, rename, recursive cleanup, archive-mode chmod, fsync, unlink, and rmdir remain descriptor-relative under captured ancestor identity.

### Final gates and live evidence

- Focused containment tests: **4 passed, 70 deselected**.
- Full reset command: `PYTHONDONTWRITEBYTECODE=1 timeout 180s uv run python -m pytest -q -W error -p no:cacheprovider tests/gateway/test_rehearsal_reset.py` -> **74 passed in 1.19s**.
- Room bootstrap compatibility: `... tests/gateway/test_telegram_room_bootstrap.py` -> **14 passed**.
- Service-state compatibility: `... tests/gateway/test_nutrition_coaching.py -k 'service_state or paused'` -> **3 passed, 149 deselected**.
- Ruff, strict `ty`, redirected-cache `compileall`, and scoped `git diff --check` passed for all four changed Python files. LSP was attempted for all four and rejected external product-repository paths as outside the task workspace root.
- Provider authentication preflight passed. Historical and supplemental profile-bound archives remain strict-valid with their unchanged 12-scope and 1-scope digests. Live baseline remained zero customers, no duplicate/control/PID/gateway-lock artifacts, four inactive zero-byte baseline locks, and three inactive dedicated service names.
- Telegram preflight remained read-only: one no-offset `getUpdates?timeout=0`, pending `10 -> 10`, no webhook, and all dedicated identity/topology/membership/admin checks passed. Redacted temporary receipt SHA-256 `6f35171ebaeb7d639a464190afc53f44c6f9b895fa4d50761f37b9144786f030`.
- Final candidate: **96 exact paths**, raw-byte digest `953023b926448232fce28bf932b66e3e3ab672de23eeac5001ca8349d3fd8813`. Changed leaf hashes: reset `b3435b5e79f57045f0ed2ab6844290e47d309cfd17d090b01b5b1852b38c90fd`; room bootstrap `cfc7c071d677a1cf396268246265601c5544dfe4a48ffdedc5aa33a66d373292`; service state `996d4b629eb21f59b9021a6b96ab2d82c0c89029f21e8de7cbb6143c59359f88`; reset tests `ea3576e9e9aef0b419f844756c8327d279f5075187d3b02215df62a7b8b29a49`.
- Dirty worktree remains 131 entries with unchanged status digest `89714c44258925cadf4612395cac60cb495871e02a89306f41e1f34bf529428c`. No unrelated bytes were modified by this repair.

## Scope correction and retained Task 21 closure (2026-08-10)

- Independent minimum-v1 scope audit `st_019fe8c5` classified the immutable Telegram digest gate added by `st_019fe774` as out of scope. The correction removed only that worker's digest contract, startup/reset/diagnostic gates, focused tests, profile marker removal, manifest classifications, and evidence appendix. The configured non-secret dedicated bot username marker and the prior in-memory authenticated bot identity plus process-consistency digest behavior are restored.
- Retained unchanged: all descriptor-lifetime and ancestor-rebinding protections, active-store containment, exact data-authority inventory, archive/quarantine/recovery handling, privacy and route containment, pending-update preservation, provider preflight, generation/delivery reliability, and prior diagnostics.
- Failing-closed characterization before correction: the obsolete live profile digest preflight rejected before any lock, client, gateway, or Telegram call.
- Automated gates: full `tests/gateway/test_rehearsal_reset.py` **74 passed**; full `test_telegram_conflict.py` plus `test_diagnostic_isolation.py` **73 passed**; env-loader precedence **6 passed**; room-bootstrap/service-state focus **17 passed**. All pytest runs treated warnings as errors. Ruff, reset-scoped strict `ty`, redirected-cache `compileall`, scoped `git diff --check`, and raw-byte manifest verification passed. Broader `ty` continues to report pre-existing diagnostics in the large Telegram/diagnostic surfaces; no diagnostic was suppressed. LSP was attempted for every affected Python file and rejected paths outside the task workspace root.
- Read-only live QA called only `getMe` and `getWebhookInfo`: profile `.env` beat both an inherited wrong token and a wrong project fallback; the profile `.env` hash remained unchanged; `getMe` succeeded; its normalized username matched the configured marker in memory; an intentionally wrong username did not match; and no webhook was configured. No raw identity or token was printed or persisted, and `getUpdates` was not called.
- Local preflight: activation and delivery flags are false; registry and service-state maps are empty; gateway metadata is stopped; PID and gateway lock are absent; the dedicated systemd service is inactive; and no process has the target `HERMES_HOME`.
- Dirty-worktree preservation: the start snapshot had 126 status entries. The final snapshot has 123; the only removed status members are the two worker-only tracked hunks now clean and the deleted worker-only identity module. No path was added and no unrelated status code changed.
- Refreshed immutable candidate: **96 exact raw-byte paths**, digest `0fa3c0d12c0e8a941eb5f44308893054b69381bf38c7a949dc2b76df4ac06ac1`; current 123-entry status snapshot SHA-256 `a317f51e7a9b065f0a5662ace57ad1a50774b87c3caf0969983998f2e55a3426`. Every leaf hash and aggregate digest was recomputed from current bytes.
- Cleanup: redirected bytecode and task temporary files were removed; no gateway, monitor, lock holder, Telegram consumer, or test profile remains. No activation, send, polling, membership mutation, route mutation, credential change, plan checkbox, ledger, Boulder, todo, commit, or Git-state operation occurred.

