I read this as a read-only Gate20 root-cause investigation - I’ll inspect the sealed evidence and code, reproduce the failing node across successor/predecessor and controlled delta variants, then report exact evidence and the smallest repair without changing the repository or evidence.


The primary Gate20 artifact mixed an interpreter dependency collection failure with a later corrected run; I’m separating those harness layers now and using the candidate’s sealed snapshot plus the corrected JUnit to test candidate causality rather than trusting the receipt’s initial attribution.


Candidate causality is now disproved: successor and predecessor fail identically under the certification launcher’s `umask 077`, while both pass under the interactive `0022` umask because the security seam rejects the fixture’s `0644` file before the injected replacement runs. I’m doing one final production-level inode trace to pin the correct assertion and repair.


I read this as a read-only Gate20 root-cause investigation - I inspected the evidence, reproduced both sealed variants, and isolated the harness-dependent test defect.

## Verdict

This is **not candidate-caused**. The failure is a pre-existing test defect exposed by the certification harness’s `umask 077`.

The test injects:

```python
os.replace(replacement, path)
```

Production correctly detects the inode replacement and raises:

```text
ValueError: overlay journal file was replaced
```

But the test then incorrectly expects `path` still to name the original inode. POSIX `os.replace` guarantees that `path` names the replacement inode. The original inode remains unchanged but unlinked.

## Evidence chain

The primary Gate20 JUnit did not contain this failure. It contained three collection errors because the Hermes interpreter lacked `typer`.

The first corrected run executed zero tests because:

```text
ERROR: Unknown config option: asyncio_default_fixture_loop_scope
```

The authoritative failure is:

```text
...-gate20-corrected-v2-st_01a00b39/artifacts/corrected-source-package.junit.xml
```

It reports 679 tests, 3 failures, including:

```text
tests.test_adaptive_nutrition
test_overlay_recovery_rejects_symlink_and_data_inode_replacement

assert b'replacement' == complete + b'{"torn":'
```

The production seam is `OverlayJournal._recover_locked`, particularly lines 7273-7282 of `adaptive_nutrition.py`, which compares the post-read named inode against the inode returned by `_read_overlay_rows_locked`.

## Hypotheses tested

1. **Candidate delta changed overlay behavior — refuted.**
   - The only successor files are:
     - `checkin_cli/customer_admin.py`
     - `checkin_cli/activation_token_rotation_policy.py`
     - `tests/test_customer_admin.py`
   - `adaptive_nutrition.py` matches the sealed snapshot at SHA-256 `d3020f...342bcb`.
   - Rebinding all three files to predecessor state gives the identical failure.

2. **Test order/global leakage — order refuted, inherited global state confirmed.**
   - The test fails alone and after its two preceding overlay tests under `umask 077`.
   - It passes alone and in order under `umask 0022`.
   - This is not leakage from another test. Both certification scripts explicitly execute `umask 077`.

3. **Harness/filesystem semantics — confirmed root cause.**
   - Under `0022`, `Path.write_bytes()` creates mode `0644`; production rejects the unsafe fixture before the monkeypatched `os.replace` executes, producing a false pass.
   - Under `0077`, files are mode `0600`, so the replacement executes. Production detects it, but the test’s final pathname assertion is invalid.

## Exact controlled results

Successor alone, isolated bwrap, inherited `0022`:

```text
1 passed in 0.09s
EXIT=0
```

Successor after preceding overlay tests, `0022`:

```text
3 passed in 0.10s
EXIT=0
```

Predecessor three-file rebind, alone and ordered, `0022`:

```text
1 passed in 0.09s
3 passed in 0.09s
```

The same four commands prefixed with `umask 077`:

```text
successor alone:       1 failed in 0.12s, EXIT=1
successor ordered:     1 failed, 2 passed in 0.12s, EXIT=1
predecessor alone:     1 failed in 0.13s, EXIT=1
predecessor ordered:   1 failed, 2 passed in 0.12s, EXIT=1
```

Representative command:

```bash
umask 077
bwrap --die-with-parent --unshare-net --ro-bind / / \
  --tmpfs /tmp --dev /dev --proc /proc \
  --setenv HOME /tmp --setenv TMPDIR /tmp \
  --setenv PYTHONDONTWRITEBYTECODE 1 \
  --setenv PYTHONPATH "$S" --chdir "$S" \
  /home/cube/miniconda3/bin/python -B -m pytest -q \
  -p no:cacheprovider \
  tests/test_adaptive_nutrition.py::test_overlay_recovery_rejects_symlink_and_data_inode_replacement
```

The predecessor command additionally read-only bound the two predecessor files and `/dev/null` over the added module.

Production-level inode trace under `umask 077`:

```text
recover_exception ValueError overlay journal file was replaced
held_mode 0o600
held_identity (84, 8)
named_identity (84, 9)
named_bytes b'replacement'
original_inode_bytes_unchanged True
EXIT=0
```

## Smallest test-first repair

Fix the test, not production or the harness:

1. Explicitly `chmod(0o600)` both manually created files so the security path is exercised regardless of launcher umask.
2. After recovery rejects the replacement, assert:
   - the named path contains `b"replacement"`;
   - optionally, through a descriptor opened before replacement, the original inode still contains `complete + b'{"torn":'`.

Conceptually:

```python
path.write_bytes(complete + b'{"torn":')
path.chmod(0o600)
replacement.write_bytes(b"replacement")
replacement.chmod(0o600)

original_fd = os.open(path, os.O_RDONLY)
try:
    with pytest.raises(ValueError, match="unsafe|replaced"):
        journal.recover()

    assert path.read_bytes() == b"replacement"
    os.lseek(original_fd, 0, os.SEEK_SET)
    assert os.read(original_fd, len(complete) + 8) == complete + b'{"torn":'
finally:
    os.close(original_fd)
```

Do not “correct” the harness by changing its umask: that would restore a false-positive pass without testing inode replacement.

No repository, candidate, evidence, Git, service, or live-state files were changed.