I read this as a read-only test-design investigation - I’ll trace the runtime credential lifecycle, existing fixtures, and failure evidence, then specify the smallest executable RED test plus the coherence regression matrix with source citations.


The live evidence narrows the defect to post-commit authority publication: the transaction committed and the successor ran, but manual QA rejected its credential as non-current/revoked. I’m now mapping that boundary onto the combined worktree’s existing fixtures and transaction fault hooks.


I read this as a read-only test-design investigation - I traced the live failure through credential loading, external authority publication, transaction commit, and rollback.

## Finding

The cheapest faithful RED is a disposable transaction test that:

1. Starts with an external authority whose current candidate is the predecessor.
2. Gives the service both real credential names.
3. Executes the v1.5 successor transaction.
4. Loads authority through the production credential loader from the successor credential directory.

Current behavior rebinds `task26-candidate-digest` but merely copies the old authority pin; it never promotes the successor in the external registry/ledger. The production loader then reaches the exact live error.

Evidence confirms this was post-commit, not startup/install failure:

- Live manual QA error: `.omo/evidence/nutricoach-v150-combined/task-v14r20-live-outcome.json:8`
- Ledger had already succeeded: same file `:11`
- Correct remediation recorded as “repair candidate-current authority publication”: same file `:18`
- Successor credential held digest `3dab...`: rehearsal credential line 1.
- Its pin still referenced the 13-event v1.3 authority: rehearsal pin line 1.
- The live drop-in loaded both `task26-authority-pin.json` and `task26-candidate-digest`: rehearsal `authority.conf:4-5`.

## Cheapest failing-first test

**File:** `tests/test_nutricoach_v150_v14_safety.py`
**Test:** `test_committed_successor_is_current_in_external_runtime_authority`

### Fixture composition

Reuse:

- `target_fixture(tmp_path)` for profile, service unit, and transaction files (`tests/test_nutricoach_v150_sealed_controller.py:26-36`).
- `write_runtime_authority(tmp_path / "external-authority", predecessor)` for a private, coherent two-event registry/ledger/pin/candidate fixture (`scripts/dualcoach_v111_disposable_fixture.py:243-283`).
- `ConcreteLiveHost.disposable(...)`, which already supports deterministic stage faults (`scripts/nutricoach_v150_concrete_host.py:90-107,130-157,220-223`).
- `load_task26_production_authority(...)`, which reads the exact production credential names and performs activation authorization (`gateway/platforms/task26_runtime_authority.py:357-389`).

Configure the current drop-in with:

```text
LoadCredential=task26-authority-pin.json:<fixture pin>
LoadCredential=task26-candidate-digest:<fixture predecessor digest>
```

Set `CREDENTIALS_DIRECTORY` to the successor’s generated `runtime-authority` directory after `execute_disposable(...)`.

### Expected RED

```python
source, candidate = load_task26_production_authority(
    profile_root=host.paths.profile,
    package_root=tmp_path / "package",
)
```

Current code raises:

```text
ValueError: runtime candidate is not current or was revoked
```

This is the exact production branch at `gateway/platforms/task26_runtime_authority.py:146-148`.

Why it fails:

- `switch_postimages()` rewrites the candidate credential for both supported names (`scripts/nutricoach_v150_host_operations.py:127-142`).
- It copies every other credential, including the stale pin, unchanged.
- External registry/ledger current-candidate state remains the predecessor.
- Production loading validates the credential candidate against that external state.

### GREEN observable

The same production load must succeed, followed by:

```python
assert candidate == host.candidate_digest
with source.authorize(candidate, "activation") as snapshot:
    assert snapshot["candidate_digest"] == host.candidate_digest
```

Also assert all four authority surfaces agree:

- successor credential digest;
- external registry `current_qualified_candidate`;
- external registry and ledger heads;
- installed pin’s `event_count`, `registry_head_sha256`, and `ledger_head_sha256`.

Finally:

```python
assert PhaseJournal(...).phase() == "COMMITTED"
assert GlobalLedger(...).outcome() == "SUCCEEDED"
```

Do not accept only “credential file contains the successor”; that is already covered by `test_switch_rebinds_task26_candidate_credential_name` (`tests/test_nutricoach_v150_v14_safety.py:239-257`) and missed the live defect.

## Required regression tests

### 1. Stale pin

**File:** `tests/test_nutricoach_v150_v14_safety.py`
**Test:** `test_runtime_authority_promotion_never_commits_a_stale_pin`

Advance the external registry/ledger to the successor while retaining the predecessor pin, then exercise production loading.

Expected fail-closed result:

```text
ValueError: runtime authority pin is stale
```

The constructor requires pin event count and both heads to equal current chains (`gateway/platforms/task26_runtime_authority.py:102-126`).

For the fixed transaction, inject failure after registry/ledger publication but before pin publication and assert:

- transaction is not `COMMITTED`;
- predecessor service is running;
- successor is absent;
- a freshly loaded predecessor credential succeeds;
- pin heads equal verified registry/ledger heads.

### 2. Interrupted promotion

**File:** `tests/test_nutricoach_v150_v14_safety.py`
**Test:** `test_interrupted_runtime_authority_promotion_recovers_coherent_authority`

Parameterize deterministic publication checkpoints, not sleeps:

```python
[
    "registry_published",
    "ledger_published",
    "registry_source_bound",
    "ledger_source_bound",
    "pin_published",
    "successor_credential_published",
]
```

These boundaries are necessary because current publication is multi-write:

- base registry then ledger: `task26_candidate_authority.py:165-166`;
- source/genesis rebinding rewrites each document again: `task26_runtime_authority.py:420-423`;
- fixture pin and candidate are separate writes: `dualcoach_v111_disposable_fixture.py:276-283`.

Each injected `BaseException`, followed by fresh-process `recover_pending(host)`, must produce:

- phase `ROLLED_BACK`;
- transaction ledger `FAILED`;
- no successor;
- predecessor active;
- `verify_candidate_authority(root, predecessor)` succeeds;
- production loading through the active runtime’s credential directory succeeds;
- pin event count and heads exactly match the paired chains.

This requires a durable promotion journal or equivalent recovery data. Restoring only profile/unit/drop-in bytes is insufficient.

### 3. Ordinary pre-commit rollback after completed promotion

**File:** `tests/test_nutricoach_v150_sealed_controller.py`
**Test:** `test_post_promotion_fault_restores_predecessor_external_authority`

Extend the existing stage matrix with a named authority-promotion stage before service activation. Inject the next-stage fault after promotion.

Assert behavioral restoration, not byte rewind:

- external current candidate is predecessor;
- any compensating append remains chain-valid;
- active predecessor pin references the new final heads;
- successor credential/root is absent;
- service is running;
- approval ledger is consumed `FAILED`.

The existing generic fault matrix already checks every concrete stage, exact profile restoration, service restart, and successor cleanup (`tests/test_nutricoach_v150_sealed_controller.py:152-189`). It should include the promotion stage and authority coherence assertions.

### 4. Post-commit/manual-QA rollback

**File:** `tests/test_nutricoach_v150_v14_safety.py`
**Test:** `test_postcommit_manual_qa_rollback_republishes_predecessor_authority`

Reproduce the live sequence:

1. Transaction reaches `COMMITTED`/`SUCCEEDED`.
2. Successor production authority load succeeds.
3. Invoke the committed rollback path with a synthetic manual-QA failure.
4. Load production authority through the restored predecessor credentials.

GREEN requires:

- phase `ROLLED_BACK`;
- one-use ledger remains `SUCCEEDED`, matching the live semantics;
- predecessor is current externally;
- predecessor credential and refreshed pin load successfully;
- successor and weekly authority are absent.

The existing evidence rollback explicitly starts from `COMMITTED` (`r20-postcommit-rollback.py:50-69`) and checks predecessor restoration and successor removal (`:76-113`), but does not test external registry/ledger/pin authority.

## Supporting unit coverage

Add **`tests/gateway/test_task26_runtime_authority_promotion.py`** with:

- `test_promotion_publishes_candidate_registry_ledger_and_pin_coherently`
- `test_stale_pin_is_rejected_after_external_head_advances`
- `test_interrupted_promotion_recovers_without_mixed_heads`
- `test_rollback_qualification_makes_predecessor_current_and_repins`
- `test_revoked_successor_cannot_be_promoted_or_requalified`

The last case preserves existing revocation semantics: revoked candidates cannot be requalified (`task26_candidate_authority.py:114-117,237-239`).

## Regression command

```bash
PYTHONPATH='dualcoach/profile:.' uv run pytest -q \
  tests/gateway/test_task26_runtime_authority_promotion.py \
  tests/test_nutricoach_v150_v14_safety.py \
  tests/test_nutricoach_v150_sealed_controller.py \
  tests/test_nutricoach_v150_v3_hardening.py \
  tests/gateway/test_nutrition_coaching.py::test_disposable_controller_issuance_enables_approved_transport
```

No tests or live probes were run because the assigned scope was strictly read-only. The proposed RED uses only disposable fixtures and the real production credential loader; it covers live credential plus external registry/ledger/pin coherence without live actions.