Symptom
Changing `pr_reviewer.repos` (or `shadow_mode`) via `POST /api/settings` persists to config and the graph reloads (`"config saved"`, `"reloaded"`), but the running dispatcher keeps its boot-time values. A manual dispatch to a newly-added repo returned `drop:unlisted-repo` until the container was restarted; after restart the same dispatch ran.
Cause
`Dispatcher.init` snapshots config into instance state:
```python
self.repos = [str(r) for r in (self.cfg.get("repos") or []) if r]
self.shadow = bool(self.cfg.get("shadow_mode", True))
self.promotion_owner = bool(self.cfg.get("promotion_owner", False))
```
The plugin's config-reload path doesn't rebuild the Dispatcher (or re-read these), so live edits are silent no-ops until a process restart. This is the "hot-reload must thread every store" class.
Impact
These read as ordinary settings (no `restart_required` signal — the core schema can't know a plugin caches them), so an operator flipping `shadow_mode` or editing `repos` believes it took effect when it didn't. For a gate flip that's a real footgun (thinking you're formal-blocking a repo you aren't).
Fix options
- Re-read `cfg` on each event (read `self.cfg["repos"]` live rather than snapshotting), or
- Rebuild the Dispatcher in the plugin's reload hook when `pr_reviewer.*` changes, or
- Minimum: register these keys as restart-required so the API surfaces it.
Found flipping Vera to the formal gate (ADR 0078 stage-2), 2026-07-13.
🤖 Generated with Claude Code
Symptom
Changing `pr_reviewer.repos` (or `shadow_mode`) via `POST /api/settings` persists to config and the graph reloads (`"config saved"`, `"reloaded"`), but the running dispatcher keeps its boot-time values. A manual dispatch to a newly-added repo returned `drop:unlisted-repo` until the container was restarted; after restart the same dispatch ran.
Cause
`Dispatcher.init` snapshots config into instance state:
```python
self.repos = [str(r) for r in (self.cfg.get("repos") or []) if r]
self.shadow = bool(self.cfg.get("shadow_mode", True))
self.promotion_owner = bool(self.cfg.get("promotion_owner", False))
```
The plugin's config-reload path doesn't rebuild the Dispatcher (or re-read these), so live edits are silent no-ops until a process restart. This is the "hot-reload must thread every store" class.
Impact
These read as ordinary settings (no `restart_required` signal — the core schema can't know a plugin caches them), so an operator flipping `shadow_mode` or editing `repos` believes it took effect when it didn't. For a gate flip that's a real footgun (thinking you're formal-blocking a repo you aren't).
Fix options
Found flipping Vera to the formal gate (ADR 0078 stage-2), 2026-07-13.
🤖 Generated with Claude Code